Docker Compose
NimbleBrain ships two container images: ghcr.io/nimblebraininc/nimblebrain-runtime (agent engine + API on port 27247, internal) and ghcr.io/nimblebraininc/nimblebrain-web (React UI served by Caddy, exposed on port 27246). Docker Compose runs both on a single machine with a shared internal network.
Prerequisites
Section titled “Prerequisites”- Docker Engine 24+ and Docker Compose v2
- An Anthropic API key
- At least 2 GB of available memory
Compose file
Section titled “Compose file”Create a project directory and add this file:
services: platform: image: ghcr.io/nimblebraininc/nimblebrain-runtime:latest build: . restart: unless-stopped volumes: - workspace:/data - ./skills:/app/skills:ro environment: - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - OPENAI_API_KEY=${OPENAI_API_KEY:-} - GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY:-} networks: - internal
web: image: ghcr.io/nimblebraininc/nimblebrain-web:latest build: web/ restart: unless-stopped ports: - "27246:8080" environment: - PLATFORM_URL=http://platform:27247 depends_on: platform: condition: service_healthy networks: - internal
volumes: workspace:
networks: internal:The platform reads its working directory from the workspace volume mounted at /data (the image sets NB_WORK_DIR=/data). Configuration (nimblebrain.json), skills, and bundle state all live under that directory — there is no separate config bind mount required to boot. Drop custom skill files into the ./skills directory if you want to mount them read-only.
What each service does
Section titled “What each service does”| Service | Image | Port | Purpose |
|---|---|---|---|
platform | ghcr.io/nimblebraininc/nimblebrain-runtime | 27247 (internal) | Agent engine, HTTP API, MCP bundle manager |
web | ghcr.io/nimblebraininc/nimblebrain-web | 27246 (host) → 8080 (container) | React SPA served by Caddy, proxies /v1/* to platform |
The web container runs Caddy, which serves the static UI files and reverse-proxies all /v1/* API requests to the platform container. The platform container is not exposed to the host — all traffic flows through the web container.
Volumes
Section titled “Volumes”| Volume/Mount | Container path | Purpose |
|---|---|---|
workspace (named volume) | /data | Working directory (NB_WORK_DIR) — conversations, logs, installed bundles, and nimblebrain.json |
./skills (bind mount, read-only) | /app/skills | Custom skill files |
Environment variables
Section titled “Environment variables”| Variable | Required | Description |
|---|---|---|
ANTHROPIC_API_KEY | Yes | Your Anthropic API key for Claude |
OPENAI_API_KEY | No | OpenAI API key (for OpenAI model provider) |
GOOGLE_GENERATIVE_AI_API_KEY | No | Google Gemini API key (for Google model provider) |
ALLOWED_ORIGINS | Production | Comma-separated list of allowed CORS origins (e.g., https://nb.example.com). Required for cross-origin cookie auth |
NB_PUBLIC_ORIGIN | Production (when using OAuth connectors) | Canonical public origin of the platform (e.g., https://nb.example.com). Used to build absolute callback URLs such as the OAuth redirect_uri for connectors. A mismatch with the URL registered in each vendor’s OAuth app produces vendor-side redirect_uri does not match errors at sign-in time. |
Authentication is configured through an instance.json adapter (dev, oidc, or workos), not an environment variable. There is no NB_API_KEY — see Security for adapter setup.
Health check
Section titled “Health check”The platform image has a built-in health check:
HEALTHCHECK --interval=30s --timeout=5s \ CMD curl -f http://localhost:27247/v1/health || exit 1The web service uses depends_on with condition: service_healthy, so it only starts after the platform passes its first health check.
Start the stack
Section titled “Start the stack”-
Set environment variables
Create a
.envfile in your project directory:.env ANTHROPIC_API_KEY=sk-ant-api03-your-key-hereOr export it directly:
Terminal window export ANTHROPIC_API_KEY=sk-ant-api03-your-key-hereThis starts the platform in dev mode (no authentication). Configure an
instance.jsonadapter before exposing it to a network — see Security. -
Start the services
Terminal window docker compose up -d✔ Network project_internal Created✔ Volume "project_workspace" Created✔ Container project-platform-1 Healthy✔ Container project-web-1 Started -
Verify
Terminal window docker compose psNAME STATUS PORTSproject-platform-1 running (healthy) 27247/tcpproject-web-1 running 0.0.0.0:27246->8080/tcpCheck the health endpoint directly (proxied through the web container):
Terminal window curl http://localhost:27246/v1/health{"status":"ok","version":"1.2.3","buildSha":"abc1234","bundles":[]} -
Open the UI
Go to http://localhost:27246. In dev mode the UI opens straight to the agent. Once an
instance.jsonadapter is configured, you sign in through that adapter’s flow (for example the OIDC provider’s hosted login).
Stop and restart
Section titled “Stop and restart”# Stop (preserves volumes)docker compose down
# Stop and remove volumes (deletes all workspace data)docker compose down -v
# Restartdocker compose restartUpdate to a new version
Section titled “Update to a new version”-
Pull the latest images
Terminal window docker compose pull -
Recreate the containers
Terminal window docker compose up -dCompose replaces only the containers whose images changed. The
workspacevolume persists across updates.
Backup and restore
Section titled “Backup and restore”Workspace data lives in the workspace named Docker volume. The default mount point is /data inside the platform container.
Backup
Section titled “Backup”docker run --rm \ -v project_workspace:/data \ -v "$(pwd)":/backup \ alpine tar czf /backup/nimblebrain-backup.tar.gz -C /data .Restore
Section titled “Restore”docker compose down
docker run --rm \ -v project_workspace:/data \ -v "$(pwd)":/backup \ alpine sh -c "rm -rf /data/* && tar xzf /backup/nimblebrain-backup.tar.gz -C /data"
docker compose up -dPlatform image details
Section titled “Platform image details”The ghcr.io/nimblebraininc/nimblebrain-runtime image is built on python:3.13-slim (Python is the base so Python-based MCP bundles run natively) and adds the Bun runtime plus the tooling bundles shell out to:
| Component | Version | Purpose |
|---|---|---|
| Python | 3.13 | Base image; runs Python-based MCP bundles |
| Bun | latest | JavaScript/TypeScript runtime (runs the platform itself) |
| Node.js + npm | 24 | Runs Node-based MCP bundles |
| Git, curl, unzip | System | Bundle clones at boot, the health check, archive extraction |
| mpak CLI | @nimblebrain/mpak@0.4.1 | Downloads and manages bundles from the mpak registry |
The container runs as a non-root user (UID 1000). The entrypoint is bun run src/cli/index.ts serve, which starts the HTTP API server on port 27247 with NB_WORK_DIR=/data and NB_HOST=0.0.0.0.
Reverse proxy
Section titled “Reverse proxy”The web container’s Caddy server already handles reverse proxying for the default setup. If you want to place your own reverse proxy in front of the stack (for TLS termination, custom domain, etc.), point it at port 27246:
server { listen 443 ssl; server_name nb.example.com;
ssl_certificate /etc/ssl/certs/nb.example.com.pem; ssl_certificate_key /etc/ssl/private/nb.example.com-key.pem;
location / { proxy_pass http://127.0.0.1:27246; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_buffering off; }}Troubleshooting
Section titled “Troubleshooting”Platform container keeps restarting
Section titled “Platform container keeps restarting”Check the logs:
docker compose logs platformCommon causes:
ANTHROPIC_API_KEYis missing or invalid- A malformed
instance.json— the server refuses to start if the auth adapter config is invalid. Check the logs for the validation error and see Security.
Web container won’t start
Section titled “Web container won’t start”The web container waits for the platform health check to pass. If the platform is unhealthy, the web container stays in a “waiting” state. Fix the platform first.
Port 27246 already in use
Section titled “Port 27246 already in use”Change the host port mapping in docker-compose.yml:
ports: - "9090:8080" # Access the UI on port 9090 instead