Skip to content

Reference deploys — Docker, Fly.io, Render

read as .md

If you followed the OSS Quick Start, you have ggui serve running on localhost. This page puts that same server behind a public URL so your phone, teammates, or collaborators can reach it. Three drop-in manifests — generic Docker, Fly.io, Render.com — are inlined below; copy them straight into your project root.

None of these deploys phone home. Your code runs on your infrastructure. Viewer clients pair with the public URL via the flow in Self-hosted pairing.

FilePurpose
DockerfileMulti-stage Node 22 image that boots ggui serve on $PORT.
fly.tomlFly.io manifest — scales to zero, healthchecks /ggui/health.
render.yamlRender.com Blueprint — Docker runtime, healthchecks /ggui/health.

All three are self-contained — there is no separate templates package to install.

Works anywhere Docker runs: EC2, your homelab, a Raspberry Pi, a Coolify VPS. Save this as Dockerfile in your project root (next to ggui.json):

# syntax=docker/dockerfile:1
# Boots `ggui serve` on $PORT. Assumes your project root has
# package.json + ggui.json (+ agent entry and blueprints).
FROM node:22-slim AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install --omit=dev
FROM node:22-slim
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY . .
EXPOSE 6781
CMD ["sh", "-c", "npx -y @ggui-ai/cli serve --host 0.0.0.0 --port ${PORT:-6781}"]
Terminal window
docker build -t my-ggui .
docker run --rm -p 6781:6781 -e PORT=6781 -e ANTHROPIC_API_KEY=sk-... my-ggui

The image exposes /ggui/health, /mcp (MCP channel), /ws (WebSocket channel for ActionEnvelope / StreamEnvelope), /pair, /admin/pair/init, /admin/pair/:pairingId/revoke, and / (operator console) on $PORT. Once running, point a viewer client at http://<docker-host>:6781 and follow the Pairing guide.

ggui serve binds plaintext HTTP. Put a TLS-terminating reverse proxy in front (Caddy, nginx, Cloudflare Tunnel) for anything beyond a LAN. The WebSocket channel rides the same :6781 port — make sure your proxy upgrades /ws correctly. Minimal Caddy example (Caddy handles WS upgrades automatically):

my-ggui.example.com {
reverse_proxy 127.0.0.1:6781
}

Fly gives you a free-tier region, HTTPS by default, and scale-to-zero. Reuse the Dockerfile from Option 1 and save this as fly.toml:

# fly.toml — `ggui serve` reference deploy
app = "my-ggui" # change me
primary_region = "iad" # change me
[build]
dockerfile = "Dockerfile"
[env]
PORT = "6781"
[http_service]
internal_port = 6781
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
[[http_service.checks]]
interval = "30s"
timeout = "5s"
grace_period = "10s"
method = "GET"
path = "/ggui/health"
Terminal window
fly launch --no-deploy # picks up the fly.toml + Dockerfile above
fly secrets set ANTHROPIC_API_KEY=sk-...
fly deploy

The [http_service] block sets force_https = true, so the URL you hand a viewer client is https://<app>.fly.dev — no proxy setup needed. The WS channel rides the same TLS endpoint as wss://<app>.fly.dev/ws.

auto_stop_machines = true + min_machines_running = 0 freezes the machine when idle and thaws it on the next request (~1–2 s cold start). Fine for a personal server. Disable it if you need zero-latency persistence — e.g., a long-running session where a cold start mid-conversation would drop the WS connection.

Render is the simplest click-to-deploy story. Commit the manifest + Dockerfile, point Render at your repo, and it redeploys on every push to main. Reuse the Dockerfile from Option 1 and save this as render.yaml:

# render.yaml — `ggui serve` reference deploy
services:
- type: web
name: my-ggui
runtime: docker
plan: starter
healthCheckPath: /ggui/health
envVars:
- key: ANTHROPIC_API_KEY
sync: false # set the value in the Render dashboard
Terminal window
git add Dockerfile render.yaml
git commit -m "chore: add ggui serve reference deploy"
git push

In Render’s dashboard: New → Blueprint → connect your repo. Render reads render.yaml, provisions the service, and gives you a https://<name>.onrender.com URL (TLS + WS upgrade handled).

Swap plan: starter for standard if your agent does heavier in-process work — embeddings, large prompt assembly, or holding many concurrent WebSocket sessions.

ggui serve honors a handful of env vars across all three deploys. Set them via the PaaS’s secret store (Fly secrets, Render environment groups, docker run -e):

VariableEffect
PORTBind port. Injected by Fly + Render automatically; pass -e PORT=… for raw Docker. The Dockerfile’s CMD forwards it via --port ${PORT:-6781}.
LLM provider keySet one of ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_API_KEY / OPENROUTER_API_KEY via the PaaS secret store, matching ggui.json#generation.model. Without one, every render falls back to a Connect-a-key card pointing at /settings. See ggui serve → Generation.
GGUI_PERSISTENT_DIROverride the .ggui/persistent/ bundle location (HMAC secrets, sessions, vectors, short-codes, paired bearers). Defaults to project-local; point at a mounted volume for durability.
GGUI_MCP_INSTRUCTIONSServer-level MCP instructions preset (default / aggressive / always / minimal / off). CLI flag --mcp-instructions wins if both are set.

Pin --admin-token via the CMD (e.g. ggui serve --admin-token "$GGUI_ADMIN_TOKEN" --host 0.0.0.0 --port $PORT) when you want the admin bearer to survive restarts; otherwise the server mints a fresh one each boot.

The default ggui serve posture is strict-auth: /mcp only accepts pair-minted bearers, and /admin/pair/init requires the per-boot ggui_admin_* token (or whatever you pin via --admin-token). That’s already safe to expose. The hardening below covers the remaining edges before handing out a public URL to paired clients:

  1. Pin the admin token. Without --admin-token, the server mints a fresh ggui_admin_* per boot — fine for local use, awkward when you redeploy. Pin a stable value via a secret (e.g. Fly secrets, Render environment groups) and pass it as --admin-token "$GGUI_ADMIN_TOKEN" in the CMD.
  2. (Optional) swap in a real AuthAdapter. If you want OIDC / Cognito / SSO instead of pair-minted bearers, wrap your agent entrypoint with createGguiServer({ auth }) that gates /mcp and /ws. See the AuthAdapter interface in @ggui-ai/mcp-server-core.
  3. Firewall /admin/*. /admin/pair/init and /admin/pair/:pairingId/revoke are the only admin surfaces today, both bearer-gated by the admin token. Restricting the /admin/ prefix to an allow-list IP or VPN at the reverse-proxy layer adds defense-in-depth (and covers any new /admin/* routes added later).
  4. NEVER pass --dev-allow-all or --public-demo here. Those flags relax /mcp to accept any bearer — or none at all (the second one adds a rate limiter and a “operator pays” banner). They exist for local-dev and demo audiences, not for a deployed server. --public-base-url is for ad-hoc tunnel testing, not for these manifests — the PaaS already gives you the public URL.
  5. Rotate pairings. Pair-minted bearers have no built-in expiry — they live until revoked. Revoke stale paired devices from the operator console at / (or POST /admin/pair/:pairingId/revoke directly).

Skip these and a “deployed ggui serve” still authenticates correctly, but you’ll be churning admin tokens on every redeploy and can’t selectively block admin-only routes at the edge.

  • Managed deploys. Reference assets only — no auto-updates, no managed rollouts, no dashboard beyond what the PaaS ships. Render’s built-in logs/metrics gets closest to “push to main → zero-downtime rollout with observability”; full production SRE is out of scope.
  • Multi-region replication. Each deploy is a single instance. ggui serve is stateful-per-process today; replicas won’t share session state, blueprint cache, or paired-device records without a shared store. storage.renders in ggui.json supports sqlite for a single node; durable multi-instance storage is roadmap.
  • Managed hosting. A managed lane — builds, logs, quotas, BYOK, billing, run by us — is coming soon. These self-hosted manifests are the available-now path.