Docs / Self-hosting

Self-hosting the full stack

The repositories ship a compose deployment targeting any VM with Docker — developed and running on Oracle Cloud's Always Free tier (Ampere ARM). The same compose file runs the edge alone (default) or the full cloud stack (gateway + console + TLS proxy) behind the cloud profile.

Architecture: every service binds 127.0.0.1; the only public surface is the TLS reverse proxy. Nothing is published to 0.0.0.0 — even the proxy uses host networking with loopback upstreams.

What you need

  • A VM with Docker + compose v2 and two sibling checkouts: free-llm (public edge) and zerofare-cloud (gateway + console).
  • Node 24 with corepack pnpm (the images build in-container, but local tooling helps for bootstrap).
  • Ports 80/443 open for ACME if you want automatic TLS via a real hostname.

Bring up the stack

  1. Provision secrets

    Create deploy/oracle-free/secrets/ with a vault passphrase, a gateway pepper (random, ≥32 bytes), and an (initially empty) seed file. All owned by UID 65532, mode 0600:

    cd zerofare-cloud/deploy/oracle-free
    mkdir -p secrets state
    umask 077
    head -c 48 /dev/urandom | base64 > secrets/gateway_pepper
    printf 'your-vault-passphrase\n' > secrets/vault_passphrase
    echo '{"version":1,"tenants":{}}' > secrets/control-seed.json
  2. Build the images

    Images build from the sibling checkouts (node image pinned by digest; ARM and x86 both covered):

    docker compose build          # edge
    docker compose --profile cloud build   # gateway + console
  3. Bootstrap a tenant

    The bootstrap tool provisions a tenant, generates both secrets, and merges digest-only records into the shared seed file. Plaintext is printed once — capture it:

    docker compose --profile cloud run --rm \
      -v ./secrets:/run/zerofare-seed-out control-plane \
      node tools/bootstrap-tenant.mjs --tenant-id ten_myco --tenant-name "My Co"

    Re-running is idempotent: it merges new keys without invalidating existing ones.

  4. Start everything

    docker compose up -d
    docker compose --profile cloud up -d

    Verify from the VM — fail-closed first:

    curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8788/management/v1/sessions   # 401
    curl -s http://127.0.0.1:8789/management/v1/healthz                                     # 200
  5. Add TLS

    Point a DNS A record at your VM (or use an sslip.io hostname while evaluating), edit the two hostnames in caddy/Caddyfile, and restart the proxy. Caddy obtains and renews Let's Encrypt certificates automatically:

    docker compose up -d caddy
    Ordering matters: open firewall ports before first starting Caddy, or ACME validation fails and backs off. If that happens, docker compose restart caddy after opening ports.

Console & management surface

  • Console: https://your-host/ — paste the management credential from bootstrap.
  • Management API: https://your-host/management/v1/… — same origin, same credential.
  • Gateway API: https://your-host:8443/… — for customer API keys and SDK baseUrl.

Upgrades & state

Service state is in-memory by design (Phase 1); the durable artifacts are the seed file and pepper, both digest-only. Restarting containers re-hydrates credentials from the seed — verified by tests and by the live deployment. Container images are rebuilt from source with in-image typecheck, so a git pull && docker compose --profile cloud build && docker compose --profile cloud up -d is the whole upgrade path.

Operations runbook

The repository's deploy/oracle-free/README.md is the detailed runbook: secret staging, bootstrap procedure, two-layer firewall rules, public verification checks, and troubleshooting (including the ACME ordering gotcha).