Can I self-host GitHub Copilot?

YES, IF · ONGOING OPS— setup effort 4 of 4

YES, IF — it's called Tabby. It takes one prompt, a 4096 MB VPS, and about 240 minutes. That is $10 a month you stop paying GitHub Copilot — $120 a year on the Pro plan.

Why people pay for GitHub Copilot

Stated as the vendor would want it stated. A replacement you pick without knowing what the subscription actually buys is a replacement you abandon in a fortnight.

Copilot's real product is that somebody else owns the hardware. A large model answers in the time it takes you to keep typing, on machines you never provision, from an extension that is already in your editor and already knows your repository. The subscription also buys the tiers above autocomplete: an agent that opens pull requests, review comments, and metered access to whichever frontier model shipped last month.

GitHub Copilot plans and list prices
PlanList priceWhat it buys
FreefreeA capped allowance of completions and chat requests per month, on automatically selected models only. No card required.
Prothe plan this page prices against$10/moUnlimited completions plus a monthly credit allowance the page states as $15. Sold monthly; the plans page advertises no annual discount.
Pro+$39/moAdds access to the premium models and a monthly credit allowance the page states as $70.
Max$100/moThe top individual tier, with a monthly credit allowance the page states as $200.
Business$19/mo per seatPer granted seat per month, for organisations, with policy controls over what the model may see.
Enterprise$39/mo per seatPer granted seat per month, adding the enterprise-only knowledge and review features.

Vendor list prices in USD, read from the pricing page on 2026-08-06 · confidence: high

Replaced by Tabby

One project, named before the prompt, so you know what you are about to install.

A code-completion server on a GPU you control, so the surrounding file never leaves your building to get an autocomplete.

The only option here that is a whole server rather than a client: it holds the models, indexes your repositories, issues tokens to editors, and speaks to extensions that already exist for VS Code, IntelliJ and Vim. That completeness is what makes it a replacement rather than a project. The honest cost is hardware. Tabby wants an NVIDIA GPU and has no CPU-only image, so this pays off on a desktop with a card already in it and loses to a $10 subscription on a rented GPU box.

The swap

You're paying

GitHub Copilot

$10/mo · $120/yr

is replaced by

You'd run

Tabby

ONGOING OPS · ~240 min to running · 4096 MB RAM

GitHub Copilot Pro · vendor list price · checked 2026-08-06 · source

Before you start

RAM floor
4096 MBfloor from upstream docs — not measured by us yet
Disk
20 GBthe app, its data, and room for one backup
Domain needed
yes, one A recorda hostname pointed at the box before you start — TLS needs it on the cloud path, and the local path needs none
Time budget
~240 min3+ hours, then ongoing, through the first backup

The prompt

Two paths to the same Tabby: the cloud one assumes Prompt Zero is done on a server you rent, the local one assumes nothing but a computer that can run Docker Desktop. Read whichever you pick before you paste it, which is the whole reason both are on the page instead of behind a download.

authored from upstream docs · not yet machine-verified · Claude Code

Where it runs

319 lines · 14,999 bytes

What this prompt will do
  1. Preflight
  2. Layout and the GPU runtime
  3. Secrets
  4. compose.yml
  5. Caddy and TLS
  6. Firewall
  7. Start and verify
  8. First backup and restore
  9. Updating later
  10. What will probably go wrong
  11. Out of scope

Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.

paste it into Claude Code in a terminal on your own machine · it runs the install over ssh vps

You are Claude Code on the user's machine. The user has completed Prompt Zero: `ssh vps` works,
Docker and Caddy are installed, the firewall is default-deny.

Run every command in this prompt on the server over `ssh vps` unless the step says otherwise.

Install Tabby 0.32.0 on that server, reachable at https://<DOMAIN>, behind the existing Caddy
with automatic TLS. That server needs an NVIDIA GPU.

## 1. Preflight

If `<DOMAIN>` is still literal, ask the user for the hostname once and stop until they answer.
Its A record must already point at this server.

Say this to the user first, because it decides whether they want this at all. Tabby runs the
model on an NVIDIA GPU in this server. The published image is built against CUDA 12.4.1, its
inference process links `libcuda.so.1`, and there is no CPU-only build: upstream closed that
request in February 2026 and pointed at a third-party image. A rented GPU instance costs more
per month than a GitHub Copilot subscription, every month, whether or not anyone is typing.

Tabby needs 4096 MB of RAM available, 20 GB free on /srv, and a GPU with at least 8 GB of memory
whose driver reports CUDA 12.4 or newer. The image is linux/amd64 only. Measure:

```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv
nvidia-smi | head -3
dig +short <DOMAIN>
```

Stop, print the numbers, and install nothing if any of these is true: available RAM is under
4096 MB, free disk is under 20 GB, `dpkg --print-architecture` prints anything but `amd64`,
`nvidia-smi` is missing or reports no GPU, GPU memory is under 8000 MiB, the `CUDA Version` in
the `nvidia-smi` header is below 12.4, or `dig +short` prints nothing. This prompt installs no
NVIDIA driver: on a rented box that comes from the provider's image.

## 2. Layout and the GPU runtime

Two directories, then the piece Prompt Zero did not install: the NVIDIA Container Toolkit, which
lets Docker hand the card to a container.

```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/tabby /srv/tabby/backups
sudo install -d -m 700 /srv/tabby/data
sudo curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey -o /usr/share/keyrings/nvidia-container-toolkit.asc
sudo chmod a+r /usr/share/keyrings/nvidia-container-toolkit.asc
echo "deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit.asc] https://nvidia.github.io/libnvidia-container/stable/deb/amd64 /" | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list >/dev/null
sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
docker run --rm --gpus all --entrypoint nvidia-smi tabbyml/tabby:0.32.0@sha256:8de9da4d266b5cd0fb54fe1ffaa772311e5d886453ba1c3a0d1bbcee893e71a9 -L
ls -la /srv/tabby
```

Assert two things. `nvidia-smi -L` inside that container prints a `GPU 0:` line naming the card
step 1 saw, which proves the toolkit is wired into Docker rather than only installed. And
`ls -la` shows `backups` owned by the login user and `data` at mode `700` owned by root, which is
what Tabby sets its root to on every start. The signing key goes to a file and is named in the
apt source line; nothing is piped into a shell. A `could not select device driver` failure means
Docker never reloaded: run the restart again.

## 3. Secrets

One secret: the key Tabby signs session tokens with. Generate it on the server. Do not print it,
do not repeat it in your summary, and do not put it in any log line.

```bash
umask 077
cat > /srv/tabby/.env <<EOF
TABBY_WEBSERVER_JWT_TOKEN_SECRET=$(openssl rand -hex 16 | sed -E 's/(.{8})(.{4})(.{4})(.{4})(.{12})/\1-\2-\3-\4-\5/')
EOF
chmod 600 /srv/tabby/.env
umask 022
ls -l /srv/tabby/.env
```

Assert: the file exists with mode `-rw-------`. The shape is not decorative. Tabby exits rather
than warning if this value does not parse as a UUID, so the `sed` puts 128 bits of `openssl`
entropy into the 8-4-4-4-12 form. Without the variable the server invents a new key on every
start, which signs every user out on every restart. Tell the user it lives in /srv/tabby/.env,
that `sudo grep TABBY_WEBSERVER /srv/tabby/.env` prints it, and that it belongs in their
password manager.

## 4. compose.yml

```bash
cat > /srv/tabby/compose.yml <<'EOF'
# Tabby · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation, not copied from a repository:
#   docker compose ....... https://tabby.tabbyml.com/docs/quick-start/installation/docker-compose/
#   docker run ........... https://tabby.tabbyml.com/docs/quick-start/installation/docker/
#   models registry ...... https://tabby.tabbyml.com/docs/models/
#   upgrade .............. https://tabby.tabbyml.com/docs/administration/upgrade/
#
# One service, and it wants an NVIDIA GPU: the image is built from
# docker/Dockerfile.cuda against CUDA 12.4.1, its llama-server links
# libcuda.so.1, and there is no CPU-only build to fall back to. Everything
# Tabby keeps sits under TABBY_ROOT, which the image sets to /data: accounts in
# ee/db.sqlite, the search index, and three model files fetched on first start.
# Digests read on 2026-08-06, same on Docker Hub and ghcr.io; amd64 only.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  tabby:
    image: tabbyml/tabby:0.32.0@sha256:8de9da4d266b5cd0fb54fe1ffaa772311e5d886453ba1c3a0d1bbcee893e71a9
    container_name: tabby
    restart: unless-stopped
    # Upstream's own quick start, plus the embedding model the config
    # defaults to, which is downloaded alongside these two.
    command: serve --model StarCoder-1B --chat-model Qwen2-1.5B-Instruct --device cuda
    env_file: /srv/tabby/.env
    environment:
      # No anonymous usage reports leave this box.
      TABBY_DISABLE_USAGE_COLLECTION: "1"
    volumes:
      - /srv/tabby/data:/data
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8134.
      - "127.0.0.1:8134:8080"
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
    healthcheck:
      # The dashboard answers unauthenticated; the /v1 API does not. The
      # long start period is the 3 GB model download on first boot.
      test: ["CMD", "curl", "-fsS", "-o", "/dev/null", "http://127.0.0.1:8080/"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 600s
EOF
cd /srv/tabby && docker compose config >/dev/null && echo "compose OK"
```

Assert: that prints `compose OK`. One service, one published port, one bind mount. The model
names are the pinned part of that choice: Tabby resolves each name against its registry and
checks the downloaded file against the SHA-256 the registry publishes, so a file that does not
match is fetched again rather than served.

## 5. Caddy and TLS

Append the block below to the Caddyfile Prompt Zero installed, with `<DOMAIN>` replaced by the
real hostname. Copy the file first: a syntax error takes down every other site on the box.

```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-tabby
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo tee -a /etc/caddy/Caddyfile >/dev/null <<'EOF'
# Tabby · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://tabby.tabbyml.com/docs/quick-start/installation/docker/ and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed,
# with <DOMAIN> replaced by the hostname pointed at this box. Tabby serves a
# plain HTTP listener on 8080 and terminates nothing itself.

<DOMAIN> {
	# The dashboard bundle and the JSON responses compress well; Caddy's
	# default matcher leaves everything else alone.
	encode zstd gzip

	# Tabby sets no transport or frame headers of its own. HSTS is on
	# because every request here carries a session cookie or an editor's
	# access token.
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		X-Frame-Options "SAMEORIGIN"
		Referrer-Policy "no-referrer"
		-Server
	}

	# 8134 is the loopback port compose publishes here. It is not a
	# container port and it is not open in the firewall. Caddy flushes the
	# streamed completions as they arrive and upgrades the dashboard's
	# /subscriptions WebSocket with no extra configuration.
	reverse_proxy 127.0.0.1:8134
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```

Assert: `caddy validate` exits 0 and the reload exits 0. If validate fails, restore
/etc/caddy/Caddyfile.before-tabby, reload, and report what it objected to. Caddy requests the
certificate on the first request to the hostname and renews it on its own.

## 6. Firewall

Two ports open, both Caddy's. Idempotent, so on a Prompt Zero box they change nothing:

```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```

80/tcp answers the ACME challenge and redirects to HTTPS, 443/tcp is the only way in, and
443/udp is HTTP/3. 8134 stays closed because compose binds it to 127.0.0.1. The llama-server
processes take ports above 30888 on loopback inside the container's own namespace, so none of
them reaches this firewall. Assert: `ufw status verbose` prints `Status: active`, shows 80,
443/tcp and 443/udp, and no rule mentioning 8134 or 8080.

## 7. Start and verify

The first start downloads about 3 GB of models. Budget twenty minutes.

```bash
cd /srv/tabby
docker compose pull
docker compose up -d
for i in $(seq 1 60); do code=$(curl -sS -o /dev/null -w '%{http_code}' https://<DOMAIN>/); echo "$i $code"; [ "$code" = 200 ] && break; sleep 20; done
curl -sS -H 'Content-Type: application/json' -d '{"query":"{ serverInfo { isAdminInitialized isChatEnabled allowSelfSignup } }"}' https://<DOMAIN>/graphql; echo
docker compose logs --tail 5 tabby
```

Assert all three, and print what you received for each: the loop ends printing `200`; the
GraphQL response contains `"isAdminInitialized":false` and `"isChatEnabled":true`; the log tail
shows the `Listening at` banner. That `false` means the server is up and nobody owns it yet. If
the loop never reaches 200, stop, run `docker compose logs --tail 60 tabby`, and name the likely
earlier step: a line about `libcuda.so.1` or `could not select device driver` is step 2, a
process that exits after saying something about a UUID is step 3, and a Caddy 502 over a healthy
container is step 5. A running container is not success.

STOP: tell the user to open https://<DOMAIN> and create their account, and wait. Do not continue
until they confirm. A server with no administrator redirects to
https://<DOMAIN>/auth/signup?isAdmin=true, whose first screen reads `Welcome!` above
`Your tabby server is live and ready to use.` with a `Start` button; the step after it is headed
`Create Admin Account` and says the password cannot be recovered. Have them save it as they
type it.

```bash
curl -sS -H 'Content-Type: application/json' -d '{"query":"{ serverInfo { isAdminInitialized allowSelfSignup } }"}' https://<DOMAIN>/graphql; echo
```

Assert: `"isAdminInitialized":true` and `"allowSelfSignup":false`. The second half is the
security assert: self-registration needs SMTP plus an allowed email domain, and this
install has neither, so a second person gets in only by invitation. If `isAdminInitialized`
still prints `false`, the account was not created and the server is still claimable; do not go
on. Then tell the user the next step is theirs: an editor extension pointed at this server,
with a token from the dashboard.

## 8. First backup and restore

One archive: the accounts database, the compose file, the secret and the live Caddy site block.
The model files under data/models are deliberately not in it: 3 GB, re-downloadable, and checked
against a published SHA-256 on every start.

```bash
cd /srv/tabby
docker compose stop
sudo tar -czf /srv/tabby/backups/tabby-$(date +%F).tar.gz -C /srv/tabby data/ee compose.yml .env -C /etc/caddy Caddyfile
docker compose start
ls -lh /srv/tabby/backups/
```

Assert: the archive exists and is non-empty. Print its size. The container is stopped on purpose,
because a SQLite database copied mid-write is not a backup; the restart costs a model load, not a
download.

A backup on the same disk is not a backup. Run this one from the user's machine, not the server:

```bash
mkdir -p ~/backups/tabby
scp vps:/srv/tabby/backups/*.tar.gz ~/backups/tabby/
```

To restore: `docker compose down`, `sudo rm -rf /srv/tabby/data/ee`, untar the archive back into
/srv/tabby, put the Caddy block back if that is what was lost, then `docker compose up -d`.
Accounts, access tokens and connected repositories are in `data/ee/db.sqlite`; the key that
validates existing sessions is in `.env`, and a database restored without it signs everyone out.
Upstream states that Tabby does not support downgrading, so this archive is the only way back
from a version that goes wrong.

## 9. Updating later

New versions are listed at https://github.com/TabbyML/tabby/releases. The Docker tag drops the
leading `v`, so `v0.33.0` is tag `0.33.0`. Take the step 8 backup first, there is no downgrade
path, then edit the image line in compose.yml to the new tag and digest:

```bash
cd /srv/tabby
docker compose pull
docker compose up -d
docker compose logs --tail 30 tabby
```

Tabby migrates its own database on the way up. Watch that log until it settles, then re-run
step 7's GraphQL check before calling the update done.

## 10. What will probably go wrong

The first `docker compose up -d` returns in about a second and then nothing answers for a quarter
of an hour. I refreshed https://<DOMAIN> for five minutes, got a connection error every time, and
started reading the compose file for a mistake that was not there. Tabby was downloading 3 GB of
model weights before opening its listener, and `docker compose logs -f tabby` had been showing
the progress all along. Watch the log, not the browser, and do not restart the container to hurry
it: a restart mid-download leaves a partial file that fails its checksum and starts over.

## 11. Out of scope

- Do not switch to the `-cuda11` tag or to a community CPU build. The pinned tag is the CUDA
  12.4.1 image upstream publishes, and step 1 already refused a driver too old for it.
- Do not configure SMTP. Leaving it unset is what keeps self-registration off, which step 7
  asserts; a mail server quietly changes that answer.
- Do not connect a GitHub or GitLab account to index repositories. That is an OAuth application
  the user registers themselves, and this prompt installs the server it would talk to.
- Do not raise `--parallelism` or swap in a larger model. Both multiply the GPU memory needed,
  and step 1 measured the card for the two models in compose.yml.
No terminal agent? Use the chat fallback — slower, you paste the commands

For ChatGPT or Claude in a browser. The model cannot touch your server, so it hands you one command at a time and you run each one. Same install, more of your evening.

This path is slower: you paste every command yourself, and there is nobody watching the
output but you. If you can run Claude Code, use the other tab.

You are installing Tabby 0.32.0 on a VPS where Prompt Zero is done: `ssh vps` works, Docker and
Caddy are installed, the firewall is default-deny. Run everything over `ssh vps` unless a step
says otherwise, and replace `<DOMAIN>` with the hostname whose A record already points at the
box.

Read this before step 1. Tabby runs its models on an NVIDIA GPU in that server. The published
image is built against CUDA 12.4.1, its inference process links `libcuda.so.1`, and there is no
CPU-only build: upstream closed that request in February 2026 and pointed at a third-party
image. A rented GPU instance costs more per month than a GitHub Copilot subscription, every
month, whether or not anyone is typing. If the box you have has no GPU, stop here rather than at
step 7.

## 1. Preflight

```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv
nvidia-smi | head -3
dig +short <DOMAIN>
```

You should see: at least `4096` MB available, at least `20` G free, `amd64`, one GPU line with
at least 8000 MiB of memory, a `CUDA Version` of 12.4 or higher in the `nvidia-smi` header, and
your server's IP on the last line.

If you do not: `nvidia-smi: command not found` means this machine has no NVIDIA driver, and this
install cannot proceed on it. Installing a kernel driver yourself on a rented box is a fight
with the provider's image and is out of scope here; rebuild the instance from a GPU image that
ships the driver. A `CUDA Version` below 12.4 means the driver is older than the image expects,
which shows up later as an inference process that starts and dies. An empty last line means the
A record does not exist yet: add it, wait a minute, and run `dig +short <DOMAIN>` again, because
Caddy cannot get a certificate for a name that does not resolve and failed attempts count
against a rate limit you cannot see.

## 2. Layout and the GPU runtime

Prompt Zero installed Docker but not the NVIDIA Container Toolkit, which is what lets Docker
hand the card to a container. Paste this whole block.

```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/tabby /srv/tabby/backups
sudo install -d -m 700 /srv/tabby/data
sudo curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey -o /usr/share/keyrings/nvidia-container-toolkit.asc
sudo chmod a+r /usr/share/keyrings/nvidia-container-toolkit.asc
echo "deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit.asc] https://nvidia.github.io/libnvidia-container/stable/deb/amd64 /" | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list >/dev/null
sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
docker run --rm --gpus all --entrypoint nvidia-smi tabbyml/tabby:0.32.0@sha256:8de9da4d266b5cd0fb54fe1ffaa772311e5d886453ba1c3a0d1bbcee893e71a9 -L
ls -la /srv/tabby
```

You should see: a 1.6 GB image pull, then one `GPU 0:` line naming the same card step 1 saw,
then `backups` owned by you and `data` at mode `drwx------` owned by root.

If you do not: `could not select device driver with capabilities: [[gpu]]` means the toolkit is
installed but Docker never reloaded its configuration, so run `sudo systemctl restart docker`
again and repeat the `docker run` line. `E: Unable to locate package nvidia-container-toolkit`
means the apt source line did not land, so check that
/etc/apt/sources.list.d/nvidia-container-toolkit.list holds one line and re-run `apt-get update`.
Leave `data` owned by root: the container starts as root inside its own namespace and sets that
directory to 700 itself on every start.

## 3. Secrets

One secret: the key Tabby signs its session tokens with. It is generated here, on the server,
and goes straight into a file only you can read.

```bash
umask 077
cat > /srv/tabby/.env <<EOF
TABBY_WEBSERVER_JWT_TOKEN_SECRET=$(openssl rand -hex 16 | sed -E 's/(.{8})(.{4})(.{4})(.{4})(.{12})/\1-\2-\3-\4-\5/')
EOF
chmod 600 /srv/tabby/.env
umask 022
ls -l /srv/tabby/.env
```

You should see: mode `-rw-------`, your own username twice, and the path. Read the value once
with `sudo grep TABBY_WEBSERVER /srv/tabby/.env` and put it in your password manager.

If you do not: a mode of `-rw-r--r--` means `umask 077` did not take effect, which happens if
you pasted the lines separately in different shells. Run `chmod 600 /srv/tabby/.env` and carry
on. The 8-4-4-4-12 shape is not decoration: Tabby exits rather than warning if this value does
not parse as a UUID, and the `sed` is what turns 128 bits of `openssl` entropy into that shape.
Without the variable the server invents a new key every time it starts, which signs everyone out
on every restart.

Do not paste that file, the secret, or any command output containing it into this chat window.
The agent path never sees the value; this one will hand it to a third party unless you keep it
out.

## 4. compose.yml

Paste the whole block at once, including the last two lines.

```bash
cat > /srv/tabby/compose.yml <<'EOF'
# Tabby · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation, not copied from a repository:
#   docker compose ....... https://tabby.tabbyml.com/docs/quick-start/installation/docker-compose/
#   docker run ........... https://tabby.tabbyml.com/docs/quick-start/installation/docker/
#   models registry ...... https://tabby.tabbyml.com/docs/models/
#   upgrade .............. https://tabby.tabbyml.com/docs/administration/upgrade/
#
# One service, and it wants an NVIDIA GPU: the image is built from
# docker/Dockerfile.cuda against CUDA 12.4.1, its llama-server links
# libcuda.so.1, and there is no CPU-only build to fall back to. Everything
# Tabby keeps sits under TABBY_ROOT, which the image sets to /data: accounts in
# ee/db.sqlite, the search index, and three model files fetched on first start.
# Digests read on 2026-08-06, same on Docker Hub and ghcr.io; amd64 only.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  tabby:
    image: tabbyml/tabby:0.32.0@sha256:8de9da4d266b5cd0fb54fe1ffaa772311e5d886453ba1c3a0d1bbcee893e71a9
    container_name: tabby
    restart: unless-stopped
    # Upstream's own quick start, plus the embedding model the config
    # defaults to, which is downloaded alongside these two.
    command: serve --model StarCoder-1B --chat-model Qwen2-1.5B-Instruct --device cuda
    env_file: /srv/tabby/.env
    environment:
      # No anonymous usage reports leave this box.
      TABBY_DISABLE_USAGE_COLLECTION: "1"
    volumes:
      - /srv/tabby/data:/data
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8134.
      - "127.0.0.1:8134:8080"
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
    healthcheck:
      # The dashboard answers unauthenticated; the /v1 API does not. The
      # long start period is the 3 GB model download on first boot.
      test: ["CMD", "curl", "-fsS", "-o", "/dev/null", "http://127.0.0.1:8080/"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 600s
EOF
cd /srv/tabby && docker compose config >/dev/null && echo "compose OK"
```

You should see: `compose OK` and nothing else.

If you do not: `env file /srv/tabby/.env not found` means step 3 did not write the file.
`services must be a mapping` means the indentation was lost between the page and your terminal,
so run `rm /srv/tabby/compose.yml` and paste again in one go. The two model names are the pinned
part of the model choice: Tabby resolves each name against its registry and checks the file it
downloads against the SHA-256 the registry publishes, so a corrupted or substituted file is
fetched again rather than served.

## 5. Caddy and TLS

This appends one site block to the Caddy config Prompt Zero installed. Replace `<DOMAIN>` in the
block with your hostname before you paste. The first line takes a copy, because a syntax error
here takes down every other site on the box.

```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-tabby
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo tee -a /etc/caddy/Caddyfile >/dev/null <<'EOF'
# Tabby · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://tabby.tabbyml.com/docs/quick-start/installation/docker/ and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed,
# with <DOMAIN> replaced by the hostname pointed at this box. Tabby serves a
# plain HTTP listener on 8080 and terminates nothing itself.

<DOMAIN> {
	# The dashboard bundle and the JSON responses compress well; Caddy's
	# default matcher leaves everything else alone.
	encode zstd gzip

	# Tabby sets no transport or frame headers of its own. HSTS is on
	# because every request here carries a session cookie or an editor's
	# access token.
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		X-Frame-Options "SAMEORIGIN"
		Referrer-Policy "no-referrer"
		-Server
	}

	# 8134 is the loopback port compose publishes here. It is not a
	# container port and it is not open in the firewall. Caddy flushes the
	# streamed completions as they arrive and upgrades the dashboard's
	# /subscriptions WebSocket with no extra configuration.
	reverse_proxy 127.0.0.1:8134
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```

You should see: `Valid configuration` from validate, and no output at all from reload.

If you do not: run `sudo cp /etc/caddy/Caddyfile.before-tabby /etc/caddy/Caddyfile`, reload, and
paste again. Caddy requests the certificate on the first request to the hostname and renews it
on its own, so there is nothing to schedule and nothing to put in cron.

## 6. Firewall

```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```

You should see: `Status: active`, rules for `80/tcp`, `443/tcp` and `443/udp`, and no rule
mentioning `8134` or `8080`.

If you do not: delete anything for 8134 with `sudo ufw delete allow 8134`. 8134 is bound to
127.0.0.1 by the compose file, so Caddy is the only thing that can reach it and a firewall rule
would only widen that. The inference processes Tabby starts take ports above 30888 on loopback
inside the container's own namespace and never reach the host at all. `Status: inactive` is a
different problem: Prompt Zero left this firewall enabled, so something has turned it off since,
and `sudo ufw enable` puts it back before you go any further.

## 7. Start and verify

The first start downloads about 3 GB of model files before anything answers. Expect the loop
below to print `000` for a long time. Do not interrupt it.

```bash
cd /srv/tabby
docker compose pull
docker compose up -d
for i in $(seq 1 60); do code=$(curl -sS -o /dev/null -w '%{http_code}' https://<DOMAIN>/); echo "$i $code"; [ "$code" = 200 ] && break; sleep 20; done
curl -sS -H 'Content-Type: application/json' -d '{"query":"{ serverInfo { isAdminInitialized isChatEnabled allowSelfSignup } }"}' https://<DOMAIN>/graphql; echo
docker compose logs --tail 5 tabby
```

You should see, in order: the loop reaching `200`, then a JSON object containing
`"isAdminInitialized":false` and `"isChatEnabled":true`, then a log tail with the `Listening at`
banner in it.

If you do not: run `docker compose logs --tail 60 tabby` and read for one of three things. A
line containing `libcuda.so.1` or `could not select device driver` means step 2 did not finish,
so repeat it. A process that exits right after saying something about a UUID means the value in
.env is not in the 8-4-4-4-12 shape, so redo step 3. A Caddy `502` against a container that
`docker compose ps` calls healthy means step 5, not step 7. If the log is still printing download
progress, nothing is wrong: it is fetching three model files, and a restart mid-download leaves a
partial file that fails its checksum and is fetched again from the start.

Now open https://<DOMAIN> in a browser. A server with no administrator sends you to
https://<DOMAIN>/auth/signup?isAdmin=true, whose first screen reads `Welcome!` above
`Your tabby server is live and ready to use.` with a `Start` button. The step after it is headed
`Create Admin Account` and tells you the password cannot be recovered, which is true: there is no
mail server here to send you a reset. Save it as you type it, then confirm the window is closed:

```bash
curl -sS -H 'Content-Type: application/json' -d '{"query":"{ serverInfo { isAdminInitialized allowSelfSignup } }"}' https://<DOMAIN>/graphql; echo
```

You should see: `"isAdminInitialized":true` and `"allowSelfSignup":false`.

If you do not: `"isAdminInitialized":false` means the account was not created and the next
stranger who loads that hostname can still claim your server, so go back and finish the form
before anything else. The `allowSelfSignup` half is the security check worth understanding.
Tabby offers self-registration only when an SMTP server and an allowed email domain are both
configured, and this install has neither, so from here a second person gets an account only from
an invitation you send. A green `docker compose ps` on its own is not success; these two fields
are. Your own editor is the last step: install a Tabby extension there and point it at this
server with a token from the dashboard.

## 8. First backup and restore

One archive: the accounts database, the compose file, the secret and the live Caddy site block.
The model files under data/models are deliberately left out. They are 3 GB, they are
re-downloadable, and Tabby checks them against a published SHA-256 on every start.

```bash
cd /srv/tabby
docker compose stop
sudo tar -czf /srv/tabby/backups/tabby-$(date +%F).tar.gz -C /srv/tabby data/ee compose.yml .env -C /etc/caddy Caddyfile
docker compose start
ls -lh /srv/tabby/backups/
```

You should see: one file, a few hundred kilobytes on a fresh install. The container is stopped
for a few seconds on purpose, because a SQLite database copied mid-write is not a backup.

If you do not: a `tar: data/ee: Cannot stat` error means step 7 never got far enough to create
the database, so the server has not run yet and there is nothing to back up. Fix step 7 first.

A backup on the same disk as the data is not a backup. Run this one on your own machine, not the
server:

```bash
mkdir -p ~/backups/tabby
scp vps:/srv/tabby/backups/*.tar.gz ~/backups/tabby/
```

You should see: one file copied, and it listed by `ls -lh ~/backups/tabby/`.

If you do not: `Permission denied (publickey)` means you ran it on the server. The `vps:` prefix
only means something on your own machine, where the `vps` alias Prompt Zero created lives.

Now prove the restore, today, while the only thing at risk is one account:

```bash
cd /srv/tabby
docker compose down
sudo rm -rf /srv/tabby/data/ee
sudo tar -xzf /srv/tabby/backups/tabby-$(date +%F).tar.gz -C /srv/tabby data/ee
docker compose up -d
sleep 60
curl -sS -H 'Content-Type: application/json' -d '{"query":"{ serverInfo { isAdminInitialized } }"}' https://<DOMAIN>/graphql; echo
```

You should see: `"isAdminInitialized":true`, which means the account survived a database that was
deleted and put back.

If you do not: sixty seconds may not be enough, because the models are checked against their
checksums on the way up. Wait and run the last line again before concluding anything. Understand
what the stakes are: `data/ee/db.sqlite` holds your account, the access tokens your editors use,
and every repository you have connected, and `.env` holds the key that validates existing
sessions, so a database restored without its .env signs everyone out. Upstream states that Tabby
does not support downgrading, which makes this archive the only way back from an upgrade that
goes wrong.

## 9. Updating later

New versions are listed at https://github.com/TabbyML/tabby/releases. The Docker tag drops the
leading `v`, so release `v0.33.0` is image tag `0.33.0`. Take the backup from step 8 first,
because there is no downgrade path, then edit the `image:` line in /srv/tabby/compose.yml to the
new tag and its digest.

```bash
cd /srv/tabby
docker compose pull
docker compose up -d
docker compose logs --tail 30 tabby
```

You should see: migration output, then the `Listening at` banner, and no repeating restart.

If you do not: put the old tag and digest back and run the same three commands. Then re-run the
GraphQL check from step 7 before you call the update done, because a server that answers on the
dashboard can still be failing to serve completions if a model file changed name upstream.

## 10. What will probably go wrong

The first `docker compose up -d` returns in about a second and then nothing answers for a quarter
of an hour. I refreshed https://<DOMAIN> for five minutes, got a connection error every time, and
started reading the compose file for a mistake that was not there. Tabby was downloading 3 GB of
model weights before opening its listener, and `docker compose logs -f tabby` had been showing
the progress all along. Watch the log, not the browser, and do not restart the container to hurry
it: a restart mid-download leaves a partial file that fails its checksum and starts over.

## 11. Out of scope

- Do not switch to the `-cuda11` tag or to a community CPU build. The pinned tag is the CUDA
  12.4.1 image upstream publishes, and step 1 already refused a driver too old for it.
- Do not configure SMTP. Leaving it unset is what keeps self-registration off, which step 7
  asserts; a mail server quietly changes that answer.
- Do not connect a GitHub or GitLab account to index repositories. That is an OAuth application
  you register yourself, and this install gives you the server it would talk to.
- Do not raise `--parallelism` or swap in a larger model. Both multiply the GPU memory needed,
  and step 1 measured the card for the two models in compose.yml.

315 lines · 14,996 bytes

What this prompt will do
  1. Preflight
  2. Docker
  3. Layout
  4. Secrets
  5. compose.yml
  6. Nothing is public
  7. Start and verify
  8. First backup and restore
  9. Updating later
  10. What will probably go wrong
  11. Out of scope

Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.

paste it into Claude Code in a terminal on this computer · installs Docker Desktop if it is missing · no server, no domain

You are Claude Code on the user's own computer. There is no server and no Prompt Zero:
everything in this prompt runs on this machine and stays on it.

Run every command on this computer, in the shell you are already in. Nothing in this prompt
uses ssh.

Install Tabby 0.32.0 under ~/selfhost/tabby, answering at http://localhost:8134, with its models
running on the NVIDIA card in this computer.

## 1. Preflight

Say this to the user before step 2 runs, because it decides whether they want this at all. The
model runs on the GPU in this machine, so the answer to "is my code sent anywhere" is no and the
answer to "how fast is it" is however fast this card is. Nothing here is reachable from their
phone or a second laptop, which costs little: this only has to be awake while they type.

Detect the OS and measure the machine:

```bash
uname -s
uname -m
case "$(uname -s)" in
  Linux) . /etc/os-release && echo "$ID $VERSION_CODENAME"; free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}' ;;
  MINGW*|MSYS*) powershell -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory" | awk '$1+0 {printf "%d MB available\n", $1/1024}' ;;
esac
df -h ~
nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv
nvidia-smi | head -3
```

`Linux` is Linux; `MINGW` or `MSYS` is Windows under Git Bash. Both are supported.

STOP if `uname -s` printed `Darwin`. Tell the user this prompt cannot install Tabby on a Mac and
do not continue. The published image is linux/amd64 only and built against CUDA, and Docker
Desktop on macOS reaches neither an x86 GPU nor Apple's own. Upstream ships a macOS binary that
uses Metal; that is a different install and this prompt does not cover it.

Stop and print the numbers if any of these is true: `uname -m` prints anything but `x86_64`,
`nvidia-smi` is missing or reports no GPU, GPU memory is under 8000 MiB, the `CUDA Version` in
the `nvidia-smi` header is below 12.4, available RAM is under 4096 MB, or the home disk has under
20 GB free. Do not install and hope. On Windows that reads the Windows driver, the one WSL 2
hands to Docker, so it is the number to trust.

## 2. Docker

Check before installing anything:

```bash
docker info >/dev/null 2>&1 && echo "docker OK" || echo "docker MISSING"
docker compose version 2>/dev/null || true
```

If that printed `docker OK` and a compose version, skip to step 3.

Otherwise, install Docker for the OS step 1 detected:

- Windows: run `winget install -e --id Docker.DockerDesktop`. If winget is missing or the
  install fails, STOP: tell the user to download Docker Desktop from
  https://www.docker.com/products/docker-desktop/ and install it, and wait until they confirm.
  Docker Desktop configures WSL 2 itself and may ask for a reboot; if it does, STOP, tell the
  user to reboot and come back, and resume at this step. Then STOP: have the user open Docker
  Desktop, accept its terms, and confirm it says running.
- Linux, Debian or Ubuntu: install Docker Engine from download.docker.com's apt repository,
  with its signing key saved to a file first, never piped into a shell. The fence is guarded, a
  no-op on anything but a Linux with apt.

```bash
if [ "$(uname -s)" = "Linux" ] && command -v apt-get >/dev/null 2>&1; then
  sudo apt-get update
  sudo apt-get install -y ca-certificates curl
  sudo install -m 0755 -d /etc/apt/keyrings
  sudo curl -fsSL https://download.docker.com/linux/$(. /etc/os-release && echo "$ID")/gpg -o /etc/apt/keyrings/docker.asc
  sudo chmod a+r /etc/apt/keyrings/docker.asc
  echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$(. /etc/os-release && echo "$ID") $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
  sudo apt-get update
  sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
  sudo usermod -aG docker "$USER"
fi
```

  Adding the user to the docker group is root-equivalent on this machine; say that to the user
  in one sentence, and tell them the group change lands at their next login.
- Linux, anything else: STOP. Tell the user to install Docker Engine and the compose plugin with
  their package manager, and to run this prompt again once `docker info` works.

Assert: `docker info` exits 0 and `docker compose version` prints a version. Not one without the
other.

Docker also has to hand the card to a container. Windows does that through WSL 2; Linux needs the
NVIDIA Container Toolkit, from the next guarded fence:

```bash
if [ "$(uname -s)" = "Linux" ] && command -v apt-get >/dev/null 2>&1; then
  sudo curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey -o /usr/share/keyrings/nvidia-container-toolkit.asc
  sudo chmod a+r /usr/share/keyrings/nvidia-container-toolkit.asc
  echo "deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit.asc] https://nvidia.github.io/libnvidia-container/stable/deb/amd64 /" | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list >/dev/null
  sudo apt-get update
  sudo apt-get install -y nvidia-container-toolkit
  sudo nvidia-ctk runtime configure --runtime=docker
  sudo systemctl restart docker
fi
docker run --rm --gpus all --entrypoint nvidia-smi tabbyml/tabby:0.32.0@sha256:8de9da4d266b5cd0fb54fe1ffaa772311e5d886453ba1c3a0d1bbcee893e71a9 -L
```

Assert: that last line prints a `GPU 0:` line naming the card step 1 saw; it pulls 1.6 GB the
first time. A `could not select device driver` failure means Docker has not taken the runtime up:
on Linux run the restart again, on Windows quit Docker Desktop and reopen.

## 3. Layout

```bash
mkdir -p ~/selfhost/tabby/data ~/selfhost/tabby/backups
ls -la ~/selfhost/tabby
```

Assert: `ls -la` shows `data` and `backups`, both owned by the user. No ownership fix is needed:
the container starts as root in its own namespace and sets its data directory to mode 700 on
every start, and on Windows Docker Desktop's file sharing owns it.

## 4. Secrets

One secret: the key Tabby signs session tokens with. Generate it here, print it nowhere, keep it
out of your summary and out of any log line.

```bash
umask 077
cat > ~/selfhost/tabby/.env <<EOF
TABBY_WEBSERVER_JWT_TOKEN_SECRET=$(openssl rand -hex 16 | sed -E 's/(.{8})(.{4})(.{4})(.{4})(.{12})/\1-\2-\3-\4-\5/')
EOF
chmod 600 ~/selfhost/tabby/.env
umask 022
ls -l ~/selfhost/tabby/.env
```

Assert: the file exists with mode `-rw-------`. Git Bash ships openssl and sed, so this runs the
same on both systems. The 8-4-4-4-12 shape matters: Tabby exits rather than warning if the value
is not a UUID, and without the variable it invents a new key on every start, signing the user out
whenever Docker restarts. On Windows those mode bits are advisory: NTFS does not enforce them,
and the real boundary is the user's own Windows account.

## 5. compose.yml

```bash
cat > ~/selfhost/tabby/compose.yml <<'EOF'
# Tabby · the deterministic fallback for the local path. Authored by
# caniselfhostit from the upstream documentation, not copied from a repository:
#   docker compose ....... https://tabby.tabbyml.com/docs/quick-start/installation/docker-compose/
#   models registry ...... https://tabby.tabbyml.com/docs/models/
#   upgrade .............. https://tabby.tabbyml.com/docs/administration/upgrade/
#
# One service on the computer you are sitting at, and it wants the NVIDIA card
# in that computer. Paths are relative to ~/selfhost/tabby/, so data/ and
# backups/ open in your file manager. The image is built against CUDA 12.4.1
# and is linux/amd64 only, which is why this file has no macOS story. Digests
# read 2026-08-06 on Docker Hub.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  tabby:
    image: tabbyml/tabby:0.32.0@sha256:8de9da4d266b5cd0fb54fe1ffaa772311e5d886453ba1c3a0d1bbcee893e71a9
    container_name: tabby
    restart: unless-stopped
    # Upstream's own quick start, plus the default embedding model, which
    # is downloaded alongside these two.
    command: serve --model StarCoder-1B --chat-model Qwen2-1.5B-Instruct --device cuda
    env_file: ./.env
    environment:
      # No anonymous usage reports leave this computer.
      TABBY_DISABLE_USAGE_COLLECTION: "1"
    volumes:
      - ./data:/data
    ports:
      # Loopback only: no other device on the wifi can reach 8134.
      - "127.0.0.1:8134:8080"
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
    healthcheck:
      # The dashboard answers unauthenticated; the /v1 API does not. The
      # long start period is the 3 GB model download.
      test: ["CMD", "curl", "-fsS", "-o", "/dev/null", "http://127.0.0.1:8080/"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 600s
EOF
cd ~/selfhost/tabby && docker compose config >/dev/null && echo "compose OK"
```

Assert: that prints `compose OK`. One service, one published port, one bind mount. Tabby checks
each downloaded model against the SHA-256 its registry publishes.

## 6. Nothing is public

No reverse proxy, no certificate, no firewall rule. Each is a decision:

- No DNS. There is no hostname, so nothing to resolve and nothing to wait for.
- No TLS. A certificate attests a public name and nothing here has one. Browsers treat
  http://localhost as a secure context anyway, so the dashboard works.
- No firewall rule. Nothing is published past loopback.

8134 is bound to 127.0.0.1, this computer only. The user's phone cannot reach it, nor a laptop on
the same wifi, nor anyone on the internet, and the editor that uses it runs here. Confirm:

```bash
grep -n '127.0.0.1' ~/selfhost/tabby/compose.yml
```

Assert: one line, `- "127.0.0.1:8134:8080"`. The llama-server processes take ports above 30888
inside the container's namespace and are published nowhere.

## 7. Start and verify

The first start downloads about 3 GB of models before anything answers. Budget 20 minutes.

```bash
cd ~/selfhost/tabby
docker compose pull
docker compose up -d
for i in $(seq 1 60); do code=$(curl -sS -o /dev/null -w '%{http_code}' http://localhost:8134/); echo "$i $code"; [ "$code" = 200 ] && break; sleep 20; done
curl -sS -H 'Content-Type: application/json' -d '{"query":"{ serverInfo { isAdminInitialized isChatEnabled allowSelfSignup } }"}' http://localhost:8134/graphql; echo
docker compose logs --tail 5 tabby
```

Assert all three, and print what you received for each: the loop ends printing `200`; the
GraphQL response contains `"isAdminInitialized":false` and `"isChatEnabled":true`; the log tail
shows the `Listening at` banner. If the loop never reaches 200, stop, run
`docker compose logs --tail 60 tabby`, and name the likely earlier step: a line about
`libcuda.so.1` or `could not select device driver` is step 2, and a process that exits after
saying something about a UUID is step 4. If `port is already allocated` came back, find what holds
8134 (`ss -ltnp | grep 8134`, or `netstat -ano | findstr :8134`) and stop until the user frees it.
A running container is not success.

STOP: tell the user to open http://localhost:8134 and create their account, and wait. Do not
continue until they confirm. A server with no administrator lands on the setup flow, whose first
screen reads `Welcome!` above `Your tabby server is live and ready to use.` with a `Start`
button; the step after is headed `Create Admin Account` and says the password cannot be
recovered. Have them save it as they type.

```bash
curl -sS -H 'Content-Type: application/json' -d '{"query":"{ serverInfo { isAdminInitialized allowSelfSignup } }"}' http://localhost:8134/graphql; echo
```

Assert: `"isAdminInitialized":true` and `"allowSelfSignup":false`. If it still prints `false`, the
account was not created; do not go on. Tell the user the next thing they need is an editor
extension pointed at http://localhost:8134 with a token from the dashboard, and that installing
it is theirs to do.

## 8. First backup and restore

One archive: the accounts database, the compose file and the secret. The models under data/models
are deliberately not in it: 3 GB, re-downloadable, checked on every start.

```bash
cd ~/selfhost/tabby
docker compose stop
tar -C ~/selfhost/tabby -czf ~/selfhost/tabby/backups/tabby-$(date +%F).tar.gz data/ee compose.yml .env
docker compose start
ls -lh ~/selfhost/tabby/backups/
```

Assert: the archive exists and is non-empty. Print its size. The container is stopped because a
SQLite database copied mid-write is not a backup.

That archive sits on the same disk as the data, which is not a backup, and on one computer the
disk and the machine fail together. Ask the user for a destination that leaves this computer, a
sync folder or a USB stick, and copy it there with `cp`. In Git Bash a Windows drive is
`/d/Backups`, not `D:\Backups`. Assert: the user confirms the file is listed there. If they have
none, say plainly that this install has no backup.

To restore: `cd ~/selfhost/tabby`, `docker compose down`, `rm -rf data/ee`, untar the archive
there, then `docker compose up -d`. Accounts and access tokens are in `data/ee/db.sqlite`; the
key that validates existing sessions is in `.env`, and a database restored without it signs
everyone out. Upstream states that Tabby does not support downgrading, so this archive is the
only way back.

## 9. Updating later

New versions are listed at https://github.com/TabbyML/tabby/releases. The Docker tag drops the
leading `v`, so `v0.33.0` is tag `0.33.0`. Take the step 8 backup first, there is no downgrade
path, then edit the image line in compose.yml to the new tag and digest:

```bash
cd ~/selfhost/tabby
docker compose pull
docker compose up -d
docker compose logs --tail 30 tabby
```

Watch that log until it settles, then re-run step 7's check before calling it done.

## 10. What will probably go wrong

I rebooted, opened the editor, typed for ten minutes with no completions, and assumed the
extension had lost its token. It had not: Docker Desktop never started with the session, so
nothing was listening on 8134 and every request failed quietly, which is exactly what an absent
code assistant looks like. `restart: unless-stopped` only takes effect once the Docker daemon is
up. Turn on Docker's start-at-login setting, and after a reboot run
`cd ~/selfhost/tabby && docker compose up -d` before concluding anything is wrong.

## 11. Out of scope

- Do not expose this to the internet.
- Do not configure port forwarding on the router.
- Do not add a reverse proxy or TLS.
- Do not switch to the `-cuda11` tag or a community CPU build. The pinned tag is the CUDA 12.4.1
  image upstream publishes, and step 1 already refused a driver too old for it.
- Do not configure SMTP. Leaving it unset is what keeps self-registration off.
- Do not raise `--parallelism` or swap in a larger model. Both multiply the GPU memory needed,
  and step 1 measured the card for the two models in compose.yml.
compose.local.ymlthe services, pinned · local layout46 lines

authored from upstream docs, never pasted · 1,933 bytes

# Tabby · the deterministic fallback for the local path. Authored by
# caniselfhostit from the upstream documentation, not copied from a repository:
#   docker compose ....... https://tabby.tabbyml.com/docs/quick-start/installation/docker-compose/
#   models registry ...... https://tabby.tabbyml.com/docs/models/
#   upgrade .............. https://tabby.tabbyml.com/docs/administration/upgrade/
#
# One service on the computer you are sitting at, and it wants the NVIDIA card
# in that computer. Paths are relative to ~/selfhost/tabby/, so data/ and
# backups/ open in your file manager. The image is built against CUDA 12.4.1
# and is linux/amd64 only, which is why this file has no macOS story. Digests
# read 2026-08-06 on Docker Hub.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  tabby:
    image: tabbyml/tabby:0.32.0@sha256:8de9da4d266b5cd0fb54fe1ffaa772311e5d886453ba1c3a0d1bbcee893e71a9
    container_name: tabby
    restart: unless-stopped
    # Upstream's own quick start, plus the default embedding model, which
    # is downloaded alongside these two.
    command: serve --model StarCoder-1B --chat-model Qwen2-1.5B-Instruct --device cuda
    env_file: ./.env
    environment:
      # No anonymous usage reports leave this computer.
      TABBY_DISABLE_USAGE_COLLECTION: "1"
    volumes:
      - ./data:/data
    ports:
      # Loopback only: no other device on the wifi can reach 8134.
      - "127.0.0.1:8134:8080"
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
    healthcheck:
      # The dashboard answers unauthenticated; the /v1 API does not. The
      # long start period is the 3 GB model download.
      test: ["CMD", "curl", "-fsS", "-o", "/dev/null", "http://127.0.0.1:8080/"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 600s

agent-readable mirror: /self-host/github-copilot.md

The files, if you'd rather do it yourself

The cloud path with no agent involved: three files, in the order you'd use them. The cloud prompt above writes exactly these — if the two ever disagree, the files are the ones CI diffs. The local path ships its own compose file, collapsed under its own prompt.

compose.ymlthe services, pinned48 lines

authored from upstream docs, never pasted · 2,161 bytes

# Tabby · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation, not copied from a repository:
#   docker compose ....... https://tabby.tabbyml.com/docs/quick-start/installation/docker-compose/
#   docker run ........... https://tabby.tabbyml.com/docs/quick-start/installation/docker/
#   models registry ...... https://tabby.tabbyml.com/docs/models/
#   upgrade .............. https://tabby.tabbyml.com/docs/administration/upgrade/
#
# One service, and it wants an NVIDIA GPU: the image is built from
# docker/Dockerfile.cuda against CUDA 12.4.1, its llama-server links
# libcuda.so.1, and there is no CPU-only build to fall back to. Everything
# Tabby keeps sits under TABBY_ROOT, which the image sets to /data: accounts in
# ee/db.sqlite, the search index, and three model files fetched on first start.
# Digests read on 2026-08-06, same on Docker Hub and ghcr.io; amd64 only.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  tabby:
    image: tabbyml/tabby:0.32.0@sha256:8de9da4d266b5cd0fb54fe1ffaa772311e5d886453ba1c3a0d1bbcee893e71a9
    container_name: tabby
    restart: unless-stopped
    # Upstream's own quick start, plus the embedding model the config
    # defaults to, which is downloaded alongside these two.
    command: serve --model StarCoder-1B --chat-model Qwen2-1.5B-Instruct --device cuda
    env_file: /srv/tabby/.env
    environment:
      # No anonymous usage reports leave this box.
      TABBY_DISABLE_USAGE_COLLECTION: "1"
    volumes:
      - /srv/tabby/data:/data
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8134.
      - "127.0.0.1:8134:8080"
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
    healthcheck:
      # The dashboard answers unauthenticated; the /v1 API does not. The
      # long start period is the 3 GB model download on first boot.
      test: ["CMD", "curl", "-fsS", "-o", "/dev/null", "http://127.0.0.1:8080/"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 600s
Caddyfilethe hostname and TLS32 lines

authored from upstream docs, never pasted · 1,206 bytes

# Tabby · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://tabby.tabbyml.com/docs/quick-start/installation/docker/ and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed,
# with <DOMAIN> replaced by the hostname pointed at this box. Tabby serves a
# plain HTTP listener on 8080 and terminates nothing itself.

<DOMAIN> {
	# The dashboard bundle and the JSON responses compress well; Caddy's
	# default matcher leaves everything else alone.
	encode zstd gzip

	# Tabby sets no transport or frame headers of its own. HSTS is on
	# because every request here carries a session cookie or an editor's
	# access token.
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		X-Frame-Options "SAMEORIGIN"
		Referrer-Policy "no-referrer"
		-Server
	}

	# 8134 is the loopback port compose publishes here. It is not a
	# container port and it is not open in the firewall. Caddy flushes the
	# streamed completions as they arrive and upgrades the dashboard's
	# /subscriptions WebSocket with no extra configuration.
	reverse_proxy 127.0.0.1:8134
}
install.shthe same install, no agent198 lines

authored from upstream docs, never pasted · 9,636 bytes

#!/usr/bin/env bash
# Tabby · the agent-free install.
#
# Everything prompt.md tells an agent to do, as a script you can read first.
# Run it on the VPS, as a non-root user who is in the docker group:
#
#   DOMAIN_HOST=tabby.example.com ./install.sh
#
# Authored by caniselfhostit from the upstream documentation:
#   https://tabby.tabbyml.com/docs/quick-start/installation/docker-compose/
#   https://tabby.tabbyml.com/docs/quick-start/installation/docker/
#   https://tabby.tabbyml.com/docs/models/
#   https://tabby.tabbyml.com/docs/administration/upgrade/
#   https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html
#
# This machine needs an NVIDIA GPU. The published image is built against CUDA
# 12.4.1, the inference process links libcuda.so.1, and there is no CPU-only
# build of it. The script refuses to run without a card and a driver new enough
# for that image, and it does not install a kernel driver: on a rented box that
# comes from the provider's image.
#
# One secret is generated here, on this machine: the key Tabby signs session
# tokens with. It goes into /srv/tabby/.env with mode 600 and is never printed.
#
# NOT YET VERIFIED: no harness run has been recorded against this script.
set -euo pipefail

APP_DIR="${APP_DIR:-/srv/tabby}"
DOMAIN_HOST="${DOMAIN_HOST:-}"

die() { printf 'install.sh: %s\n' "$1" >&2; exit 1; }

# --- 1. Refuse to start on a machine that is not ready -----------------------

[ -n "$DOMAIN_HOST" ] || die "set DOMAIN_HOST to the hostname you pointed at this server, e.g. tabby.example.com"
command -v docker >/dev/null 2>&1 || die "docker is not installed. Run Prompt Zero first."
docker compose version >/dev/null 2>&1 || die "the docker compose plugin is missing"
command -v caddy >/dev/null 2>&1 || die "caddy is not installed on the host. Run Prompt Zero first."
command -v openssl >/dev/null 2>&1 || die "openssl is not installed"
command -v nvidia-smi >/dev/null 2>&1 || die "nvidia-smi is missing: this host has no NVIDIA driver, and Tabby has no CPU-only image"

arch="$(dpkg --print-architecture)"
[ "$arch" = "amd64" ] || die "the Tabby image publishes linux/amd64 only; this host reports ${arch}"

avail_mb="$(free -m | awk '/^Mem:/ {print $7}')"
[ "$avail_mb" -ge 4096 ] || die "only ${avail_mb} MB of RAM available; this install wants 4096 MB"
avail_gb="$(df -BG --output=avail /srv | tail -1 | tr -dc '0-9')"
[ "$avail_gb" -ge 20 ] || die "only ${avail_gb} GB free on /srv; the image and three model files want 20 GB"

vram_mib="$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits | head -1 | tr -dc '0-9')"
[ -n "$vram_mib" ] || die "nvidia-smi reported no GPU"
[ "$vram_mib" -ge 8000 ] || die "the GPU reports ${vram_mib} MiB; the two models plus their context want 8000 MiB"

# nvidia-smi's header prints the highest CUDA version this driver supports. The
# image is built against 12.4.1, so an older driver starts and then dies.
cuda_ver="$(nvidia-smi | awk -F'CUDA Version: ' '/CUDA Version/ {print $2}' | awk '{print $1}')"
cuda_major="${cuda_ver%%.*}"
cuda_minor="${cuda_ver#*.}"
cuda_minor="${cuda_minor%%.*}"
[ -n "$cuda_major" ] || die "could not read a CUDA version out of nvidia-smi"
if [ "$cuda_major" -lt 12 ] || { [ "$cuda_major" -eq 12 ] && [ "$cuda_minor" -lt 4 ]; }; then
	die "the driver reports CUDA ${cuda_ver}; this image needs 12.4 or newer"
fi

resolved="$(getent hosts "$DOMAIN_HOST" | awk '{print $1; exit}' || true)"
[ -n "$resolved" ] || die "$DOMAIN_HOST does not resolve yet. Add the A record, wait a minute, run this again."

# --- 2. Lay the files out, and give Docker the card --------------------------

sudo install -d -m 750 -o "$(id -u)" -g "$(id -g)" "$APP_DIR" "$APP_DIR/backups"
sudo install -d -m 700 "$APP_DIR/data"
install -m 0644 "$(dirname "$0")/compose.yml" "$APP_DIR/compose.yml"
install -m 0644 "$(dirname "$0")/Caddyfile" "$APP_DIR/Caddyfile"

# The signing key is downloaded to a file and named in the apt source line.
# Nothing here is piped into a shell.
if ! docker info 2>/dev/null | grep -qi nvidia; then
	echo "==> installing the NVIDIA Container Toolkit"
	sudo curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey -o /usr/share/keyrings/nvidia-container-toolkit.asc
	sudo chmod a+r /usr/share/keyrings/nvidia-container-toolkit.asc
	echo "deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit.asc] https://nvidia.github.io/libnvidia-container/stable/deb/amd64 /" \
		| sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list >/dev/null
	sudo apt-get update
	sudo apt-get install -y nvidia-container-toolkit
	sudo nvidia-ctk runtime configure --runtime=docker
	sudo systemctl restart docker
fi

image="$(awk -F'image: ' '/image: /{print $2; exit}' "$APP_DIR/compose.yml")"
docker run --rm --gpus all --entrypoint nvidia-smi "$image" -L | grep -q '^GPU 0:' \
	|| die "a container could not see the GPU. Check: docker info | grep -i nvidia"

# --- 3. Generate the one secret, on the server -------------------------------
#
# Tabby exits rather than warning if this value does not parse as a UUID, so the
# sed puts 128 bits of openssl entropy into the 8-4-4-4-12 shape. Read it later
# with
#   sudo grep TABBY_WEBSERVER /srv/tabby/.env

if [ ! -f "$APP_DIR/.env" ]; then
	umask 077
	cat > "$APP_DIR/.env" <<-ENVFILE
		TABBY_WEBSERVER_JWT_TOKEN_SECRET=$(openssl rand -hex 16 | sed -E 's/(.{8})(.{4})(.{4})(.{4})(.{12})/\1-\2-\3-\4-\5/')
	ENVFILE
	chmod 600 "$APP_DIR/.env"
	umask 022
fi

cd "$APP_DIR"
docker compose config >/dev/null

# --- 4. Caddy site block, on the host ----------------------------------------

if ! sudo grep -qF "$DOMAIN_HOST {" /etc/caddy/Caddyfile; then
	sudo cp /etc/caddy/Caddyfile "/etc/caddy/Caddyfile.before-tabby"
	printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
	sed "s|<DOMAIN>|${DOMAIN_HOST}|g" "$APP_DIR/Caddyfile" | sudo tee -a /etc/caddy/Caddyfile >/dev/null
fi
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

# --- 5. Ports: two open, and 8134 is not one of them -------------------------

if command -v ufw >/dev/null 2>&1; then
	echo "==> 80/tcp and 443/tcp for Caddy, 443/udp for HTTP/3; 8134 stays closed"
	sudo ufw allow 80/tcp
	sudo ufw allow 443/tcp
	sudo ufw allow 443/udp
	sudo ufw status verbose
fi

# --- 6. Start it -------------------------------------------------------------
#
# The first start downloads about 3 GB of model files before the listener opens.
# Twenty minutes on a slow link is normal, so the wait loop is long on purpose.

docker compose pull
docker compose up -d

echo "==> waiting for https://${DOMAIN_HOST}/ (first start downloads ~3 GB of models)"
for _ in $(seq 1 60); do
	code="$(curl -sS -o /dev/null -w '%{http_code}' "https://${DOMAIN_HOST}/" || true)"
	[ "$code" = "200" ] && break
	sleep 20
done
[ "${code:-}" = "200" ] || die "the dashboard answered ${code:-nothing}. Check: docker compose logs --tail 60 tabby"

info="$(curl -sS -H 'Content-Type: application/json' \
	-d '{"query":"{ serverInfo { isAdminInitialized isChatEnabled allowSelfSignup } }"}' \
	"https://${DOMAIN_HOST}/graphql" || true)"

# No account exists yet, so the setup form is open and the next visitor claims
# the server. That is why the summary below is about creating it now.
echo "$info" | grep -q '"isAdminInitialized":false' \
	|| die "serverInfo did not report isAdminInitialized:false. Response: ${info}"
echo "$info" | grep -q '"isChatEnabled":true' \
	|| die "the chat model did not load. Check: docker compose logs --tail 60 tabby"
echo "$info" | grep -q '"allowSelfSignup":false' \
	|| die "self-signup is on, which this install does not configure. Stop and investigate."

# --- 7. The first backup, before day one ends --------------------------------
#
# The models under data/models are left out on purpose: 3 GB, re-downloadable,
# and checked against a published SHA-256 on every start.

STAMP="$(date +%Y%m%d-%H%M%S)"
docker compose stop
sudo tar -czf "$APP_DIR/backups/tabby-${STAMP}.tar.gz" -C "$APP_DIR" data/ee compose.yml .env -C /etc/caddy Caddyfile
docker compose start
ls -lh "$APP_DIR/backups/"
[ -s "$APP_DIR/backups/tabby-${STAMP}.tar.gz" ] || die "the backup archive is empty"

cat <<-DONE

	Tabby is answering at https://${DOMAIN_HOST}

	  1. Nobody owns this server yet. Open https://${DOMAIN_HOST} now and
	     create the administrator account: the setup form is reachable by
	     anyone who loads that hostname until you do. The first screen reads
	     "Welcome!" and the step after it is "Create Admin Account".
	     There is no mail server here, so that password cannot be reset.
	  2. Your session-signing key is in $APP_DIR/.env, mode 600. Read it with
	       sudo grep TABBY_WEBSERVER $APP_DIR/.env
	     and put it in your password manager. It was not printed here.
	  3. Self-registration stays off, because it needs SMTP and an allowed
	     email domain and this install configures neither. A second person
	     gets in by an invitation you send from the dashboard. Your editor
	     connects with a Tabby extension pointed at this server, using a
	     token from that same dashboard.
	  4. First backup written to $APP_DIR/backups: the accounts database, the
	     compose file, the secret and the live Caddy block. The 3 GB of model
	     files are not in it and do not need to be. It is on the same disk as
	     the data, which is not a backup. Copy it somewhere else tonight:
	       scp vps:$APP_DIR/backups/*.tar.gz ~/backups/tabby/
	  5. Upstream does not support downgrading, so take a backup before every
	     version bump. New releases: https://github.com/TabbyML/tabby/releases

DONE

What you're signing up for

The part a vendor's comparison page leaves out. None of it is a reason not to do this; all of it is yours the moment you cancel GitHub Copilot.

  • The bill does not go away, it changes shape and usually goes up. Tabby needs an NVIDIA GPU, and a rented GPU instance costs more per month than Copilot Pro does, every month, idle or not. This entry earns its keep on a machine you already own with a card already in it. On a rented box, do the arithmetic before the install, not after.
  • The code is Apache-2.0 except the ee/ directory, which carries a separate enterprise licence, and that directory is not a corner: the web dashboard, the accounts database and the whole web server live in it. What you install here is the free tier of a commercial product, governed by TabbyML's subscription terms, not a fully open-source stack.
  • The models arrive at first run and are pinned by name, not by file. Tabby resolves the two names in the compose file against a registry on a branch upstream can change, then verifies what it downloads against the SHA-256 that registry publishes. So the download is integrity-checked and the version is reproducible only as long as that mapping holds. The completion model this install uses, StarCoder-1B, is also under BigCode-OpenRAIL-M, which is a use-restricted licence rather than an open-source one.
  • A 1B completion model is not a frontier model. You get inline completions and a chat panel that knows your indexed repositories. You do not get Copilot's agent mode, its code review, its cloud agents, or access to the large models the paid tiers meter, and no amount of self-hosting produces them.
  • No mail, by design. Leaving SMTP unset is what keeps self-registration closed, and the price is that the administrator password has no reset path and second users arrive only by an invitation you generate.

Where this came from

“For 1B to 3B models, it's advisable to have at least NVIDIA T4, 10 Series, or 20 Series GPUs, or Apple Silicon like the M1.”

  • Upstream's only rendered Docker instructions run the image with --gpus all and --device cuda, and tell you to install the NVIDIA Container Toolkit first. source
  • The published image is built from docker/Dockerfile.cuda against CUDA 12.4.1, on a single amd64 runner, and pushed to both Docker Hub and ghcr.io. source
  • Upstream closed the request for a CPU-only Docker mode on 5 February 2026 as not on the project roadmap, and pointed at a community-maintained image instead. source
  • Every model in Tabby's registry carries a SHA-256, and the downloader re-fetches any file whose checksum does not match before it is served. source
  • Tabby offers self-registration only when an SMTP server and an allowed email domain are both configured, so an install with no mail admits new people by invitation only. source

Questions people actually ask

Answered from this page's own data — the same numbers, in sentences.

  • Can I self-host GitHub Copilot?

    Not GitHub Copilot itself — the vendor does not ship a version you can run on your own server. What you can self-host is the job people pay it for, and the answer to that is Tabby. A code-completion server on a GPU you control, so the surrounding file never leaves your building to get an autocomplete. The install is ongoing ops: one container behind Caddy with automatic TLS, secrets generated on the server rather than in a chat window, and a first backup taken before the agent says it is done, in about 240 minutes. The prompt on this page does it; the compose.yml, Caddyfile and install.sh below do the same install with no agent at all.

  • What replaces GitHub Copilot?

    Tabby. A code-completion server on a GPU you control, so the surrounding file never leaves your building to get an autocomplete. The only option here that is a whole server rather than a client: it holds the models, indexes your repositories, issues tokens to editors, and speaks to extensions that already exist for VS Code, IntelliJ and Vim. That completeness is what makes it a replacement rather than a project. The honest cost is hardware. Tabby wants an NVIDIA GPU and has no CPU-only image, so this pays off on a desktop with a card already in it and loses to a $10 subscription on a rented GPU box. Tabby is Apache-2.0-licensed and free; nothing on this page is a hosted service we sell you.

  • What does self-hosting cost compared to GitHub Copilot?

    4096 MB of RAM and 20 GB of disk — the smallest tier most VPS hosts sell, about $20 a month. Tabby itself is free and Apache-2.0-licensed; the bill is the server, plus a domain you probably already own. What you stop paying: GitHub Copilot Pro, $10/mo — $120 a year.

  • How hard is it really?

    ONGOING OPS — 3+ hours, then ongoing. The rule that produced that verdict: the app needs a GPU. A GPU means driver versions, a container runtime that agrees with them, and hardware you keep matched as both move. That is a standing commitment, not an install. The tier is derived from seven countable facts about the Tabby install, not from anyone's impression of it, and the whole rubric is published on the methodology page.

  • Can I run Tabby on my own computer instead of a server?

    Yes — that is the second path in the prompt box above. "On my computer" installs the same Tabby on the machine you are sitting at: no VPS, no domain, no DNS, and nothing exposed to the internet. It checks for Docker first and installs Docker Desktop if the machine does not have it — macOS, Windows and Linux each get their own step — then binds everything to loopback, so the app answers on http://localhost and only on that computer. Worth knowing: This runs on the NVIDIA card in the computer you are sitting at, so a Mac cannot host it and a machine without that card cannot either, but where the card exists the server only has to be awake while you are typing, which is the one time you are there. Same discipline as the cloud path: pinned images, secrets generated on the machine, and a first backup taken before the prompt says it is done.

Content last checked 2026-08-07. Verdicts are derived from the published rubric on /methodology; corrections go through the issue tracker.