Skip to content

Getting Started

Quick start

Runs on Linux and macOS: it reads uname and skips whatever does not exist on the host. On macOS there is no systemd, apt, usermod or NVIDIA stack, so step 2 only checks that Docker Desktop is running and step 3 is a no-op; sudo is unnecessary there (compose runs as you).

cp .env.example .env           # secrets (+ DOMAIN when tunneling)
sudo ./setup.sh --dry-run      # print the plan, write nothing
sudo ./setup.sh                # asks which services to run, then builds + up
# localhost only. Expose it, and/or add a disk and GPU:
sudo ./setup.sh --tunnel
sudo ./setup.sh --tunnel --disk /nvme0n1-disk --nvidia   # linux-only steps
# skip the prompt and name the services outright:
sudo ./setup.sh --with code-server --with opencode --with openchamber

setup.sh is idempotent — re-running is safe. It is also the only thing you run outside docker: the host installs Docker and (optionally) a disk layout, the NVIDIA stack, and a tunnel. Everything else lives in docker-compose.yml.

Cookbook — first run on your host

Six checkpoints, each with a known-good result: stop and fix before the next.

0. Check the ground

sudo ss -ltnp | grep -E ':(8080|4096|3000)'   # occupied = the old stack
docker compose version                        # compose plugin must exist

Leave the old stack running for now — it keeps remote access alive until step 5.

1. Configure

cp .env.example .env

Set the UI passwords always; DOMAIN and TUNNEL_TOKEN only if you will run with --tunnel. Lost the token?

sudo tr '\0' '\n' < /proc/$(pgrep -f cloudflared | head -1)/cmdline

2. Dry run

sudo ./setup.sh --dry-run

Expect every command prefixed [dry ], tunnel off — cloudflared stays down (or ingress rows rendered for *.<your domain> when you passed --tunnel), and tunnel credentials found. Nothing is written.

3. Real run

sudo ./setup.sh              # localhost-only
sudo ./setup.sh --tunnel     # + cloudflared and the routes

Four steps; the first image build takes minutes. The last one is two commands — docker compose build then up -d — which setup.sh runs for you.

4. Verify inside out

docker compose ps                              # +1 when tunneling
docker compose logs cloudflared | grep -i registered   # only with --tunnel
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/   # 302 -> login
curl -s -o /dev/null -w '%{http_code}\n' -L http://127.0.0.1:8080/login   # 200
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4096/   # 200
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000/   # 200
docker compose exec opencode opencode --version
docker compose exec openchamber openchamber --version
docker compose exec -T openchamber node -e \
  "fetch('http://opencode:4096/').then(r=>console.log(r.status)) \
   .catch(e=>console.log('ERR',e.message))"

The last one proves both DNS-dependent pieces: service-name resolution on the dev network and the openchamber → opencode wiring.

5. Verify outside in, then cut over

Skip this if you started without --tunnel — there are no routes to check.

Load https://code.<domain>, https://opencode.<domain> and https://chamber.<domain>. Once they serve the new stack:

sudo systemctl disable --now cloudflared "code-server@$USER"

Rollback: docker compose down and re-enable those two units.

6. When it breaks

symptom cause
cloudflared restarting forever empty/wrong TUNNEL_TOKEN — read its logs
address already in use old code-server holds 8080: stop it or re-port
cloudflared logs lookup code-server it is not on the dev network
502 on a subdomain target down, or hostname missing from ingress/dashboard
code-server cannot write dirs made root-owned: sudo chown -R $USER ~/workspaces
images gone after --disk they stayed in /var/lib/docker — see Storage

Develop without deploying

To poke at the repo without provisioning a host at all:

docker run -it --rm \
  -v ~/.ssh:/root/.ssh:ro \
  -v ~/.local/share/opencode:/root/.local/share/opencode \
  -v ~/workspaces:/workspaces \
  -w /workspaces \
  ghcr.io/anomalyco/opencode

Next: Services — what can run and how to pick it.