---
title: Reference deploys — Docker, Fly.io, Render
description: Copy-paste Dockerfile, fly.toml, and render.yaml manifests for standing up `ggui serve` on a public URL.
---

:::note[Self-hosted path]
These manifests boot `ggui serve` — the open ggui protocol server. They do NOT require an account. A managed hosted lane is coming soon; these self-hosted manifests are the available-now path.
:::

If you followed the [OSS Quick Start](/oss-quickstart/), 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](/self-hosted/pairing/).

## What's in the box

| File          | Purpose                                                             |
| ------------- | ------------------------------------------------------------------- |
| `Dockerfile`  | Multi-stage Node 22 image that boots `ggui serve` on `$PORT`.       |
| `fly.toml`    | Fly.io manifest — scales to zero, healthchecks `/ggui/health`.      |
| `render.yaml` | Render.com Blueprint — Docker runtime, healthchecks `/ggui/health`. |

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

## Option 1 — Generic Docker

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`):

```dockerfile
# 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}"]
```

```bash
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](/self-hosted/pairing/).

### TLS

`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):

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

## Option 2 — Fly.io

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`:

```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"
```

```bash
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.

## Option 3 — Render.com

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`:

```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
```

```bash
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.

## Environment variables

`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`):

| Variable                | Effect                                                                                                                                                                                          |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PORT`                  | Bind port. Injected by Fly + Render automatically; pass `-e PORT=…` for raw Docker. The Dockerfile's CMD forwards it via `--port ${PORT:-6781}`.                                                |
| LLM provider key        | Set 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](/cli/serve/#generation-bring-your-own-key). |
| `GGUI_PERSISTENT_DIR`   | Override 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_INSTRUCTIONS` | Server-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.

## Before you leave localhost

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`](https://www.npmjs.com/package/@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.

## What this does NOT include

- **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](/glossary/) 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.

## What's next

- **[Self-hosted pairing](/self-hosted/pairing/)** — pair viewer clients to your deployed server.
- **[OSS Quick Start](/oss-quickstart/)** — the local-first walkthrough (run on localhost before you deploy).