Deployment
Gig'MCP deploys as a Docker Compose stack: a single Go gateway binary, a Next.js dashboard, and (for OIDC development) a local Zitadel identity provider. This page covers what each service does, the capabilities and security options the gateway container needs, and what to watch when upgrading.
For a first run, start with the quickstart. For every environment variable, see configuration.
Requirements
- Linux runtime. The sandbox layer (bubblewrap, network namespaces, seccomp) is Linux-only. On macOS the stack runs inside Docker — the gateway refuses to start where bubblewrap is unavailable.
- Docker with Compose.
- A sibling checkout of the registry repo. The gateway's
go.modreplacesgithub.com/gigmcp/registry/schemawith../gigmcp-registry/schema, anddocker-compose.ymlsupplies it as a named build context (additional_contexts: { registry: ../gigmcp-registry }). A plain build needsdocker build --build-context registry=../gigmcp-registry ..
No privileged mode and no SYS_ADMIN are required. The gateway container needs only cap_add: NET_ADMIN plus the relaxed security options below — all already set in docker-compose.yml.
Container privileges, explained
| Setting | Why |
|---|---|
cap_add: NET_ADMIN | The egress proxy creates a veth pair per sandbox and moves the peer end into the bwrap child's network namespace. |
security_opt: seccomp=unconfined | bwrap needs unprivileged user namespaces, which Docker's default seccomp profile blocks. A scoped Docker profile is a follow-up item; inside each sandbox an application-level seccomp-BPF filter closes the nested-userns escape (see sandbox isolation). |
security_opt: apparmor=unconfined | Same reason — Docker's default AppArmor profile blocks bwrap's namespace creation. |
security_opt: systempaths=unconfined | Unmasks /proc so bwrap can mount a fresh procfs in each sandbox's PID namespace. |
The Compose stack
docker-compose.yml defines three services:
services:
issuer-proxy: # socat sidecar — owns the network namespace, publishes :8080
gateway: # the Go gateway, joins the sidecar's netns
web: # Next.js dashboard, publishes :3000
volumes:
gigmcp-data: # gateway state, mounted at /data
issuer-proxy
An alpine/socat sidecar that exists for OIDC development: the gateway's configured issuer URL is http://localhost:8082 (the exact string go-oidc verifies against Zitadel's discovery document), but inside the gateway container localhost is the container itself. The sidecar owns the network namespace the gateway joins and runs socat listening on localhost:8082, forwarding to the zitadel service from docker-compose.dev.yml.
Because the gateway shares this container's network namespace (network_mode: "service:issuer-proxy"), the gateway's port 8080 is published on the sidecar, not on the gateway service — only the netns owner can publish ports.
gateway
The single Go binary: MCP aggregator, auth authority, REST API, vault, sandbox supervisor, and embedded MITM egress proxy. Built as static CGO_ENABLED=0 binaries (gateway, echo-mcp, bootstrap) on a debian:bookworm-slim base that adds bubblewrap, ca-certificates, and iptables (the gateway sets the FORWARD policy to DROP at startup as best-effort defense in depth).
Key settings baked into the image:
| Setting | Value |
|---|---|
GIG_DB_PATH | /data/gigmcp.db (SQLite) |
GIG_BOOTSTRAP_PATH | /usr/local/bin/bootstrap |
GIG_ECHO_BIN | /usr/local/bin/echo-mcp (legacy/dev fallback backend) |
| Exposed port | 8080 (MCP endpoints + REST API) |
The egress proxy listens internally on GIG_PROXY_PORT (default 8081); it is reached only from sandbox network namespaces and is not published.
Two environment variables are required — the compose file fails fast if they are unset:
GIG_BEARER_TOKEN=$(openssl rand -hex 24) \
GIG_MASTER_KEY=$(openssl rand -hex 32) \
docker compose up --build -d
GIG_MASTER_KEY is the master KEK that wraps every per-secret DEK in the credential vault. If you lose it, every stored credential becomes undecryptable. Keep it stable across upgrades and back it up separately from the database volume.
Compose environment variables are visible via docker inspect. GIG_OIDC_CLIENT_SECRET supports a GIG_OIDC_CLIENT_SECRET_FILE variant for Docker secrets; broader *_FILE support for other secrets is planned but not yet wired.
web
The Next.js dashboard as a pure frontend in its own container, published on port 3000. It proxies /api/* and /mcp/* to the gateway via server-side rewrites — see dashboard.
The rewrite target (GATEWAY_INTERNAL_URL) is baked at build time by next.config.ts — the standalone Next.js server ignores the runtime environment variable. Compose passes it as a build arg set to http://issuer-proxy:8080 (the sidecar's DNS name, since the gateway shares its network namespace). If you change the gateway's address, rebuild the web image.
Persistent state
One named volume, gigmcp-data, mounted at /data in the gateway container. It holds:
- the SQLite database (
/data/gigmcp.db): servers, manifests, users, sessions, profiles, encrypted credentials, audit log - extracted server rootfs/binaries from registry installs (
GIG_DATA_DIR, default/data)
Postgres as an alternative store backend is a design decision (repository interface, "no Postgres-only features in core") but the gateway currently wires SQLite only; there is no optional Postgres service for the gateway in the compose file today.
OIDC dev fixture (docker-compose.dev.yml)
docker-compose.dev.yml runs a local Zitadel IdP (with its own Postgres) for OIDC development. It is explicitly not for production.
docker compose -f docker-compose.dev.yml up -d
- Zitadel console:
http://localhost:8082/ui/console(first-instance adminadmin/Password1!) - Pinned to
ghcr.io/zitadel/zitadel:v2.71.0— the last release bundling Console and Login UI in one container
:::warning Start order matters
Both compose files live in the same directory, so they share one compose project and one default network. Bring up docker-compose.dev.yml (Zitadel) first, then the main stack — the gateway performs a fatal 10-second OIDC discovery at boot. Never pass --remove-orphans to the main stack; it would tear down the Zitadel containers, which belong to the same project.
:::
See authentication for the OIDC configuration block and what happens when it is unset.
TLS and public URLs
The gateway itself serves plain HTTP on :8080; TLS termination is up to your reverse proxy. Two settings interact with that choice:
GIG_PUBLIC_URL— the browser-facing origin. When it starts withhttps://, the gateway sets theSecureflag on session cookies. When OIDC is enabled andGIG_PUBLIC_URLis unset, it is derived fromGIG_OIDC_REDIRECT_URL(scheme + host).GIG_OIDC_REDIRECT_URL— must point at the browser-facing origin. When the dashboard is the entry point, that is e.g.http://localhost:3000, since the dashboard rewrites/api/*to the gateway.
Upgrading
docker compose up --build -d
What persists, and what to expect:
- Database and installed servers persist in
gigmcp-data. Sessions live in the database and survive restarts. - In-flight OIDC logins do not survive a restart — the flow cookie is signed with a per-process random key. Users simply log in again.
- Keep
GIG_MASTER_KEYidentical across upgrades (see the warning above). - Manifest changes force re-consent. If an installed server's registry manifest changed, the gateway logs that the server requires re-consent and does not spawn it until consent is re-recorded (fail closed).
- Each upgrade restarts all sandboxes; profile runtimes respawn lazily on the next request (see profiles).
Verifying the deployment
claude mcp add --transport http gigmcp http://localhost:8080 \
--header "Authorization: Bearer <your GIG_BEARER_TOKEN>"
With the default build you should see one tool, echo_echo, served by an MCP server running in an egress-isolated bubblewrap sandbox. Then open http://localhost:3000 for the dashboard (requires OIDC to be configured).