Tabby code assistant on Civo L40S GPUs: Private corporate Copilot

Deploy a self-hosted Tabby code completion server on a Civo L40S GPU node in under an hour: Qwen2.5-Coder 7B running on your own infrastructure, ghost-text completions in VS Code, and your code never leaving the cluster.

11 minutes reading time

Written by

Mostafa Ibrahim
Mostafa Ibrahim

Software Engineer at GoCardless

Every time a developer pauses mid-function, their editor sends the surrounding code to an external server. For teams working on proprietary algorithms, unreleased features, or internal APIs, this creates a real data governance question — and for regulated industries like finance or healthcare it can be a policy violation.

Commercial code completion tools also price per seat, which means costs scale directly with headcount regardless of how heavily each developer actually uses the tool. For large or fast-growing teams, that model adds up quickly.

Tabby is a self-hosted, open-source inline code completion server that runs entirely inside your own infrastructure with no external database and no cloud dependency. Your VS Code extension points to your server, completions stream back from your own GPU, and your code never leaves the cluster.

This tutorial covers inline code completion specifically, the ghost-text experience that appears as you type and is accepted with a single Tab keypress. Not chat, not agentic workflows, just fast, private, accurate completions in your editor.

The model doing the work is Qwen2.5-Coder 7B, and it fits this stack for three concrete reasons:

By the end of this tutorial, you will have a Tabby inference server running on a Civo Kubernetes cluster, a VS Code extension pointed at it, and inline ghost-text completions streaming back from a GPU that you control.

What you’ll build

A private AI code completion server on Civo Kubernetes that accepts requests from your IDE and streams inline suggestions back from a dedicated GPU node. Your code stays inside the cluster at every step.

The flow works like this:

  1. You type a function signature in VS Code and pause
  2. The Tabby VS Code extension captures the surrounding context and sends it to your cluster over a kubectl port-forward connection
  3. The Tabby pod running on the L40S GPU node passes the context to Qwen2.5-Coder 7B and streams tokens back
  4. Ghost-text completions appear inline in your editor, accepted with a single Tab keypress

The core components:

  • A Civo Kubernetes cluster with a dedicated L40S GPU node pool running Tabby inference
  • A Tabby server pod configured with Qwen2.5-Coder 7B, handling all completion requests from the team
  • A Kubernetes persistent volume caching the model weights, so the pod restarts instantly without re-downloading 15 GB
  • A Kubernetes Deployment manifest managing the Tabby pod, pinned to the L40S node via a node selector

Typing a function signature is the only manual step in the entire flow.

Prerequisites

Before you run the first command, make sure the following are in place. The tutorial assumes all four tools are installed, and your Civo account has GPU access approved.

  • A Civo account with L40S GPU quota approved. GPU quota is not enabled by default, so contact Civo support to request it before you begin.

L40S GPU availability

L40S GPU nodes are available in select Civo regions. This tutorial uses NYC1. If you are in a different region, check GPU node availability in the Civo dashboard before starting. If the an.g1.l40s.kube.x1 size is not listed, contact Civo support to request access.

  • Civo CLI manages your cluster from the terminal
  • kubectl communicates with your cluster after provisioning
  • Helm installs the NVIDIA GPU Operator
  • VS Code where completions will appear

Tested versions

This tutorial uses the NYC1 region throughout. Substitute your preferred region in all CLI commands if different.

ToolVersion

Civo CLI

v0.9.x

Kubernetes

v1.35

Helm

v3.14

Tabby

v0.32

Project structure

tabby-civo/
kubernetes/
tabby-namespace.yaml # namespace for all Tabby resources
tabby-pvc.yaml # persistent volume for model cache
tabby-deployment.yaml # Tabby pod and ClusterIP service

Once all four tools are installed and your GPU quota is confirmed, you have everything you need to follow this tutorial from start to finish.

How it fits together

Before touching the terminal, it helps to see how the pieces connect. There are four layers involved, and each one has a single responsibility. 

Tabby Code Assistant on Civo L40S GPUs: Private Corporate Copilot

The L40S GPU node is where all the computing happens. Kubernetes schedules the Tabby pod exclusively on this node using a node selector, so the GPU is never shared with system workloads. The 48 GB of VRAM holds the full Qwen2.5-Coder 7B model in FP16, leaving enough headroom for the KV cache that buffers concurrent completion requests.

The Tabby server pod sits on top of the GPU node and does three things: loads the model weights on startup, exposes an HTTP API that the IDE extension talks to, and manages user authentication tokens. It is configured entirely through a Kubernetes Deployment manifest that pins the pod to the L40S node via a node selector.

The persistent volume attaches to the Tabby pod and caches the model weights to disk. Without it, every pod restart triggers a fresh download of 15 GB of weights. With it, the pod restarts in seconds.

The port-forward connection bridges your local VS Code to the cluster. You run kubectl port-forward once, and the Tabby VS Code extension treats localhost:8080 as its endpoint. No ingress, no TLS configuration, no DNS record needed for this deployment.

The request flow:

  1. You pause mid-function in VS Code
  2. The extension captures the surrounding context and sends it to localhost:8080
  3. The port-forward tunnels the request to the Tabby pod on the cluster
  4. Tabby passes the context to Qwen2.5-Coder 7B and streams tokens back
  5. Ghost text appears in your editor

Every piece runs inside your Civo cluster. Nothing in that flow touches an external server.

Cluster sizing and cost 

This deployment uses two nodes. The CPU node runs system pods and the port-forward process. The L40S GPU node runs the Tabby inference pod exclusively, with Qwen2.5-Coder 7B sitting at roughly 14 GB of the available 48 GB VRAM.

For a 5-hour test, the total comes to roughly $6.45. You pay for the hours the node runs and nothing else.

Creating the cluster

Before deploying anything, you need a Kubernetes cluster with two node pools: one CPU node for system workloads and one L40S GPU node for Tabby inference.

Authenticate the Civo CLI and create the cluster:

civo apikey save my-key YOUR_API_KEY_HERE
civo apikey use my-key
civo kubernetes create tabby-cluster \
--size=g4s.kube.small \
--nodes=1 \
--region=NYC1 \
--wait

If the CLI times out, run civo kubernetes ls --region=NYC1 and confirm the cluster shows ACTIVE with All Workers Up: True before moving on.

Save the kubeconfig so kubectl can connect to the cluster:

civo kubernetes config tabby-cluster --region NYC1 --save --switch

Now add the L40S GPU node pool:

civo kubernetes node-pool create tabby-cluster \
--size=an.g1.l40s.kube.x1 \
--nodes=1 \
--region=NYC1

Wait 3 to 5 minutes, then verify both nodes are Ready:

kubectl get nodes
Tabby Code Assistant on Civo L40S GPUs: Private Corporate Copilot

Both nodes must show Ready before proceeding. If the L40S node stays NotReady after 5 minutes, run kubectl describe node NODE_NAME and check the Events section at the bottom for the cause.

Installing the NVIDIA GPU operator

Without the GPU Operator, nvidia.com/gpu: 1 in the deployment manifest is not recognized, and the Tabby pod will stay in Pending indefinitely.

helm repo add nvidia https://helm.ngc.nvidia.com/nvidia
helm repo update

Install the GPU operator:

helm upgrade --install gpu-operator \
-n gpu-operator --create-namespace \
nvidia/gpu-operator \
--set driver.enabled=true \
--set toolkit.enabled=false \
--set devicePlugin.enabled=true \
--set gfd.enabled=true \
--set operator.defaultRuntime=containerd \
--set validator.cuda.runtimeClassName=nvidia

toolkit.enabled=false is intentional. Civo nodes come with the container toolkit pre-configured, and enabling it again causes conflicts.

Wait 5 minutes, then verify all pods are Running or Completed:

kubectl get pods -n gpu-operator
Tabby Code Assistant on Civo L40S GPUs: Private Corporate Copilot

Confirm the GPU is allocatable:

kubectl get nodes \
-o custom-columns="NAME:.metadata.name,GPU:.status.allocatable.nvidia\.com/gpu"
Tabby Code Assistant on Civo L40S GPUs: Private Corporate Copilot

The L40S node must show 1 before moving on.

Configuring the Kubernetes manifests 

Three manifest files define the full Tabby deployment. Create them inside the kubernetes/ folder before applying anything. 

Namespace

A dedicated namespace keeps all Tabby resources isolated from system workloads and makes cleanup a single command. 

Create kubernetes/tabby-namespace.yaml:

apiVersion: v1
kind: Namespace
metadata:
name: tabby

Every resource in this tutorial lives inside the tabby namespace. Deleting the namespace at the end removes all of them in one shot. 

PersistentVolumeClaim

The PVC caches the Qwen2.5-Coder 7B model weights to disk. Without it, every pod restart re-downloads approximately 15 GB of weights. With it, restarts take seconds. Civo provisions the volume automatically via its CSI driver when the pod first schedules. 

Create kubernetes/tabby-pvc.yaml:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: tabby-model-cache
namespace: tabby
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi

50 GB gives the model weights room with space to spare for the SQLite database Tabby uses for user accounts and auth tokens. 

Deployment and service

The nodeSelector pins the pod to the L40S node. The nvidia.com/gpu: 1 resource limit tells Kubernetes to schedule it only on a node with an available GPU. The securityContext ensures the pod can write the SQLite database in the mounted volume. The readinessProbe on /v1/health prevents the pod from showing Ready until the model is fully loaded. 

Create kubernetes/tabby-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
name: tabby
namespace: tabby
spec:
replicas: 1
selector:
matchLabels:
app: tabby
template:
metadata:
labels:
app: tabby
spec:
securityContext:
fsGroup: 0
runAsUser: 0
nodeSelector:
nvidia.com/gpu.present: "true"
containers:
- name: tabby
image: registry.tabbyml.com/tabbyml/tabby:latest
command:
- /opt/tabby/bin/tabby
args:
- serve
- --model
- Qwen2.5-Coder-7B
- --device
- cuda
ports:
- containerPort: 8080
resources:
limits:
nvidia.com/gpu: 1
memory: 32Gi
requests:
memory: 16Gi
volumeMounts:
- name: model-cache
mountPath: /data
readinessProbe:
httpGet:
path: /v1/health
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
volumes:
- name: model-cache
persistentVolumeClaim:
claimName: tabby-model-cache
---
apiVersion: v1
kind: Service
metadata:
name: tabby
namespace: tabby
spec:
selector:
app: tabby
ports:
- protocol: TCP
port: 8080
targetPort: 8080
type: ClusterIP

The ClusterIP service gives the Tabby pod a stable internal address at port 8080. It has no external IP and is not reachable from outside the cluster. The port-forward in the next step is what bridges your local machine to it.

Deployment and Secure Exposure

With the three manifest files in place, you have everything Kubernetes needs to schedule the Tabby pod, provision the volume, and expose the service internally. Apply them in order; the namespace must exist before the PVC, and the PVC must exist before the deployment tries to mount it. 

Apply all three manifests:

kubectl apply -f kubernetes/tabby-namespace.yaml
kubectl apply -f kubernetes/tabby-pvc.yaml
kubectl apply -f kubernetes/tabby-deployment.yaml

Watch the pod start:

kubectl get pods -n tabby -w
Tabby Code Assistant on Civo L40S GPUs: Private Corporate Copilot

The first startup is slow. Tabby pulls the container image (~1.7 GB) and then downloads the Qwen2.5-Coder 7B weights (~15 GB). Expect 15 to 20 minutes on a fresh PVC. Tail the logs to watch the download progress:

kubectl logs -f deployment/tabby -n tabby

Wait for this line before proceeding: Listening at http://0.0.0.0:8080

Tabby Code Assistant on Civo L40S GPUs: Private Corporate Copilot

On subsequent restarts, the model loads from the PVC in under 30 seconds.

Note: If the pod crashes on first start with the pool timed out while waiting for an open connection, a previous failed run left the SQLite WAL file in a locked state. Clear it by running kubectl exec -it POD_NAME -n tabby -- rm /data/ee/db.sqlite-wal /data/ee/db.sqlite-shm, then force delete the pod with kubectl delete pod POD_NAME -n tabby --force. The pod will restart cleanly. 

Administrative setup

Tabby requires an admin account before it accepts IDE connections. Without one, every request, including the health check, returns a 401. The web UI handles the initial setup and token generation, and the only way to reach it at this stage is through a port-forward since there is no external endpoint yet. 

Run a port-forward to access the web UI: 

kubectl port-forward svc/tabby 8080:8080 -n tabby
Tabby Code Assistant on Civo L40S GPUs: Private Corporate Copilot

Open http://localhost:8080 in your browser. You will be prompted to create the first admin account. Set a username and password, then log in.

Tabby Code Assistant on Civo L40S GPUs: Private Corporate Copilot

To generate an authentication token for your IDE:

  1. Click your profile icon in the top right
  2. Go to Account Settings
  3. Copy the token shown there
Tabby Code Assistant on Civo L40S GPUs: Private Corporate Copilot

With the admin account created and the token in hand, the server is ready to accept IDE connections. 

Connecting the IDE and testing the flow

The port-forward must stay running in a separate terminal for the extension to reach the cluster. Open a new terminal and start it before continuing. 

Install the Tabby extension from the VS Code Marketplace. 

Open the Command Palette (Ctrl+Shift+P) and run:

Tabby: Connect to Server

Enter the following when prompted:

  • Endpoint: http://localhost:8080
  • Token: the token you copied from Account Settings

After connecting, the Tabby status bar item in the bottom-right corner of VS Code shows a connected indicator.

Tabby Code Assistant on Civo L40S GPUs: Private Corporate Copilot

Testing the flow

Open a new Python file in VS Code and type the following function signature:

def parse_user_records(records: list[dict], date_format: str) -> list[dict]:

Pause typing after the colon. Within 1 to 3 seconds, ghost text appears inline, suggesting the full function body. Press Tab to accept.

Tabby Code Assistant on Civo L40S GPUs: Private Corporate Copilot

On subsequent requests, latency drops to under 300 ms. If no ghost text appears:

  1. Check that the port-forward is still running in the other terminal
  2. Run kubectl get pods -n tabby and confirm the pod is 1/1 Running
  3. Check that the Tabby status bar item in VS Code is showing connected

Verify the GPU is serving completions:

# Confirm the server is responding
curl http://localhost:8080/v1/health
# Confirm authentication is working
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8080/v1/health
Tabby Code Assistant on Civo L40S GPUs: Private Corporate Copilot

The response should include "device":"cuda" and "cuda_devices":["NVIDIA L40S"].

What's next

The deployment you built here is intentional in its simplicity: one node, one pod, port-forward access. A few natural extensions from here:

  • Add more team members by going to the Tabby admin panel and generating a token per developer. Each connects their VS Code extension to the same endpoint.
  • Expose over HTTPS by replacing the port-forward with a Civo LoadBalancer and an Nginx ingress with TLS. The cluster endpoint becomes a stable URL your whole team can hit without running a local tunnel.
  • Enable chat alongside completions by adding --chat-model Qwen2.5-Coder-7B-Instruct to the deployment args. Both models fit within the L40S VRAM budget, and Tabby's web UI gains a chat panel for code explanation and review.
  • Index your repositories by connecting Tabby to your private Git repositories for RAG-based context awareness. Completions become aware of your internal APIs, naming conventions, and existing code patterns.
  • Autoscale during office hours by adding a second L40S node pool and configuring a Kubernetes HorizontalPodAutoscaler. The second node spins up at the start of the workday and scales back down at night.

Summary

Your code never left your infrastructure. Every completion request went from your editor to your cluster and back, with no third-party API, no data retention policy to read, and no audit trail on someone else's server.

The economics are straightforward. A single L40S node at $1.29 per hour is a flat infrastructure cost. It does not increase as more developers connect, which is structurally different from per-seat SaaS tools that charge per user per month.

The operational surface stayed small. One cluster, one GPU node, three manifest files, and a port-forward. Nothing required a web console, a managed service account, or a support ticket to provision.

Mostafa Ibrahim
Mostafa Ibrahim

Software Engineer at GoCardless

Mostafa Ibrahim is a software engineer and technical writer specializing in developer-focused content for SaaS and AI platforms. He currently works as a Software Engineer at GoCardless, contributing to production systems and scalable payment infrastructure.

Alongside his engineering work, Mostafa has written more than 200 technical articles reaching over 500,000 readers. His content covers topics including Kubernetes deployments, AI infrastructure, authentication systems, and retrieval-augmented generation (RAG) architectures.

View author profile