Deployment

Docker Quickstart

MCP Ambassador runs as a single Docker container. Start everything with one command.


Requirements

  • Docker Engine 20+
  • docker compose v2 (docker compose not docker-compose)

Quick start

git clone https://github.com/mcpambassador/server.git
cd server
cp .env.example .env
docker compose up

Default ports

PortPurpose
8443Client API (HTTPS) — Ambassador Client connects here
9443Admin + User Web UI (HTTPS) — browser access

First boot behavior

On first docker compose up:

  1. TLS certificates auto-generated (self-signed, stored in ./data/certs/)
  2. SQLite database initialized at ./data/ambassador.db
  3. Credential master key generated at ./data/credential_master_key (permissions 0600)
  4. Session HMAC secret generated and persisted (survives restarts)
  5. 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 needs sudo.

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.

SettingWhy
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_addSETUID/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-privilegesProtects 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: trueThe root filesystem is read-only; /data, /config and /tmp are the only writable or mounted paths.
deploy.resources.limits.pids: 4096Container-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

  1. Back up ./data, ./config, ./cache and your compose file.
  2. Empty the ./cache directory (the /tmp mount) 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.
  3. Check that ./data is owned by uid 1000 (chown -R 1000:1000 ./data). ./config/ambassador-server.yaml must be readable by uid 1000; keep it private (mode 0600). The config is a read-only mount, so the entrypoint cannot change it.
  4. Update your compose file to match the settings above (user, cap_drop, cap_add, read_only, no-new-privileges, deploy.resources.limits.pids).
  5. Remove MCP_AMBASSADOR_CHILD_UID (and _GID/_HOME if you set them; the range mode ignores those two) from your compose environment: and any env_file. The image sets a default MCP_AMBASSADOR_CHILD_UID_RANGE, and setting both makes the entrypoint exit. To keep the legacy single-uid mode on purpose, set MCP_AMBASSADOR_CHILD_UID and also empty the range (MCP_AMBASSADOR_CHILD_UID_RANGE=).
  6. 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_SECRET and CREDENTIAL_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
Previous
Audit Logs