Deployment
Docker Quickstart
MCP Ambassador runs as a single Docker container. Start everything with one command.
Requirements
- Docker Engine 20+
docker composev2 (docker composenotdocker-compose)
Quick start
git clone https://github.com/mcpambassador/server.git
cd server
cp .env.example .env
docker compose up
Default ports
| Port | Purpose |
|---|---|
8443 | Client API (HTTPS) — Ambassador Client connects here |
9443 | Admin + User Web UI (HTTPS) — browser access |
First boot behavior
On first docker compose up:
- TLS certificates auto-generated (self-signed, stored in
./data/certs/) - SQLite database initialized at
./data/ambassador.db - Credential master key generated at
./data/credential_master_key(permissions 0600) - Session HMAC secret generated and persisted (survives restarts)
- Initial credentials written to
./data/initial-credentials.txt(admin API key and recovery token). Record them, then delete the file. It is owned by uid 1000, so reading and deleting it on the host needssudo.
There are no default user accounts. Open https://localhost:9443 and the first-run setup wizard asks you to create the administrator.
Watch for these log lines:
[Server] Initialization complete — listening on :8443 (client) :9443 (admin)
docker-compose.yml overview
Version requirement
These settings require v0.10.0 or later. Upgrade the image and the compose file together: user: "0:0" on a 0.9.x image runs the server and every MCP as root. Never set it on a 0.9.x image.
The docker-compose.yml shipped in the server repository is the reference. Since v0.10.0 it sets the security options below, and they are required: each downstream MCP runs under its own unprivileged uid, which needs a small set of Linux capabilities.
services:
mcpambassador-server:
build: .
image: mcpambassador-server:latest
ports:
- '8443:8443'
- '127.0.0.1:9443:9443'
volumes:
- ./data:/data
- ./config:/config:ro
- ./cache:/tmp
environment:
MCP_AMBASSADOR_DATA_DIR: /data
NODE_ENV: ${NODE_ENV:-production}
# MCP_AMBASSADOR_LOG_LEVEL: debug # uncomment for verbose logs
# Optional, see Configuration > Downstream MCP isolation:
# MCP_AMBASSADOR_CHILD_UID_RANGE: 20000-20999
# MCP_AMBASSADOR_CHILD_NPROC: 256
security_opt:
- no-new-privileges:true
user: '0:0' # the entrypoint drops to uid 1000; no root process remains
cap_drop:
- ALL
cap_add:
- SETUID
- SETGID
- KILL
- CHOWN
- SETPCAP
read_only: true
deploy:
resources:
limits:
pids: 4096
restart: unless-stopped
The real file also sets the health check, CPU and memory limits, and the secrets and registry variables. Do not remove the lines above.
| Setting | Why |
|---|---|
user: "0:0" | The container starts as root only to run docker/entrypoint.sh, which prepares the per-MCP home directories and then runs the server as uid 1000. No root process remains. |
cap_drop: [ALL] + cap_add | SETUID/SETGID/KILL let the server start each MCP under its own uid and stop it. CHOWN and SETPCAP are used by the entrypoint only. |
no-new-privileges | Protects the server itself from privilege gain. With isolation on (the default), MCP children also get --no-new-privs from the launcher regardless, but keep this setting: the server logs a warning once, at the first MCP spawn check, when it runs without it. |
read_only: true | The root filesystem is read-only; /data, /config and /tmp are the only writable or mounted paths. |
deploy.resources.limits.pids: 4096 | Container-wide process cap. Use deploy.resources.limits.pids, not pids_limit: pids_limit conflicts with a deploy.resources.limits block in Compose, so keep just one. |
If the capabilities are missing
If SETUID, SETGID or KILL are not available, MCP children refuse to start (fail closed) and the server logs a clear message. If SETPCAP is missing, the container refuses to start. They never fall back to running as the server's uid.
The image runs tini as PID 1 (as uid 1000) so orphaned MCP grandchildren are reaped. Do not add init: true: Docker's init would be a root PID 1.
The image also ships uv/uvx (pinned), so registry MCPs that run via uvx <package>==<version> work without extra setup. See MCP Catalog.
Upgrading from 0.9.x
- Back up
./data,./config,./cacheand your compose file. - Empty the
./cachedirectory (the/tmpmount) before the first 0.10.0 boot. Old caches were written as uid 1000 and are not usable by the per-MCP uids; MCP packages are downloaded once again. - Check that
./datais owned by uid 1000 (chown -R 1000:1000 ./data)../config/ambassador-server.yamlmust be readable by uid 1000; keep it private (mode0600). The config is a read-only mount, so the entrypoint cannot change it. - Update your compose file to match the settings above (
user,cap_drop,cap_add,read_only,no-new-privileges,deploy.resources.limits.pids). - Remove
MCP_AMBASSADOR_CHILD_UID(and_GID/_HOMEif you set them; the range mode ignores those two) from your composeenvironment:and anyenv_file. The image sets a defaultMCP_AMBASSADOR_CHILD_UID_RANGE, and setting both makes the entrypoint exit. To keep the legacy single-uid mode on purpose, setMCP_AMBASSADOR_CHILD_UIDand also empty the range (MCP_AMBASSADOR_CHILD_UID_RANGE=). docker compose up --build -d, then check/health.
See Release notes for the full list of changes.
Production deployment
See Production Setup for:
- Keeping
NODE_ENV=production(the default in the reference compose file; no dev client key is created) - Configuring CA-signed TLS certificates
- Setting
ADMIN_SESSION_SECRETandCREDENTIAL_MASTER_KEY - Persistent data volume management
- Reverse proxy configuration (nginx/Caddy/Cloudflare Tunnel)
Health check
curl -k https://localhost:8443/health
# → {"status":"ok"}
Build from source
# Clone the repo
git clone https://github.com/mcpambassador/server.git
cd mcpambassador_server
# Build packages
pnpm install
pnpm -r build
# Build Docker image
docker build -t mcpambassador-server:local .
# Run
docker compose up