Docker Setup
This guide covers deploying MCPHub using Docker, including development and production configurations.Quick Start with Docker
Using Pre-built Image
The standard MCPHub image includes Node.js/pnpm, Python, uv/uvx, Git, and build tools. Use thelatest-full image if you need Cargo and the stable Rust toolchain for Rust-based MCP servers.
/app/data/mcp_settings.json. Mount the entire data directory to persist settings, users, credential bindings, and their encryption key. For custom servers, create data/mcp_settings.json before the first launch, or edit the existing file afterward. With detached mode (-d), run docker logs mcphub to see the generated initial admin password.
Existing file mounts at /app/mcp_settings.json are detected automatically and take precedence over the data directory, so existing Docker commands keep working. An explicit MCPHUB_SETTING_PATH always takes precedence over both locations. For full persistence, migrate the settings file and its existing .credentials.json and .credentials.key sidecars together into the mounted data directory. Explicit MCPHUB_SETTING_PATH values remain supported.
Building from Source
Building with Extended Features
The Docker image supports anINSTALL_EXT build argument to include additional tools. Published -full images are built with this enabled:
What’s included with INSTALL_EXT=true:
- Stable Rust + Cargo: Installed through rustup for Rust-based MCP servers.
- Docker Engine: Full Docker daemon with CLI for container management. The daemon auto-starts when the container runs in privileged mode.
- Chrome + Firefox for Playwright (amd64 only): For browser automation tasks
Docker Compose Setup
Basic Configuration
Create adocker-compose.yml file:
Persisting the stdio package cache
stdio servers download and install their package on first use: npx through npm’s
caches, uvx and other uv-based commands through uv’s. The image sets no USER, so it
runs as root and those installs land in:
Environments created by
uv tool install live in ~/.local/share/uv/tools instead. Those
are baked into the image layer and are not affected by any of this. If you run the image as
a non-root user, ~ is not /root: adjust both the paths above and the mount targets below
to that user’s home directory.
None of these are volumes by default, so they live in the container’s writable layer.
That layer is discarded whenever the container is recreated — an image bump,
docker compose down && up -d, or any change to a service’s configuration. The next
start then reinstalls every stdio server from scratch, in parallel, before any of them
can complete their MCP handshake.
Mounting the two paths keeps the installs on the host:
docker compose down; only down -v removes them. A bind mount
(./data/npm-cache:/root/.npm) also works and is easier to inspect or clear by hand, but it
is not initialised from the image the way a named volume is — it hides whatever the image
already holds at that path. On amd64 the -full image bakes a Playwright install
into /root/.npm/_npx, so a bind mount there costs that download once.
A few things worth knowing:
- Version pinning is not required. An unpinned
npx -y <package>reuses the package already installed in the cache instead of reinstalling it. npm still revalidates the package metadata against the registry on each run, so the registry (or your proxy) must stay reachable — the cache removes the install cost, not the network round trip. - The first start after adding the volumes is still cold. The volume begins empty, so that run pays the full install cost once; every recreate afterwards reuses it.
- Budget for the size. Around 90
stdioservers occupy roughly 2 GB. - To recover from a corrupted install, use a server’s Reinstall action — available
for
npxanduvxservers only. Foruvxit refreshes just that package, through a--refreshflag on the next spawn. Fornpxit deletes the whole shared/root/.npm/_npxdirectory, so every npx server reinstalls on its next start, not only the one you targeted. The_cacachevolume is what keeps that reinstall cheap.
stdio servers, a cold recreate left
19 of them connected and 74 failing their connect with MCP error -32001: Request timed out, because every server was competing to install at once. The same restart with the
cache mounted reached 73 connected and no timeouts — though that run also raised
INIT_TIMEOUT from its 300000 default to 900000, so the two changes have to be read apart.
Against the per-minute connect curve they split roughly as: the cache is worth about +45
servers inside the original 300-second budget, the longer timeout about +9. The dominant
cost was reinstalling packages, not waiting.
Production Configuration with Nginx
Environment Variables
Create a.env file for Docker Compose:
Development Setup
Development Docker Compose
Createdocker-compose.dev.yml:
Development Dockerfile
CreateDockerfile.dev:
Running the Application
Development Mode
Production Mode
Configuration Management
MCP Settings Volume Mount
Before the first launch, createdata/mcp_settings.json:
Secrets Management
For production, use Docker secrets:Data Persistence
Database Backups
Add backup service to yourdocker-compose.yml:
scripts/backup.sh:
Monitoring and Health Checks
Health Check Endpoint
Add to your application:Docker Health Checks
Monitoring with Watchtower
Add automatic updates:Troubleshooting
Common Issues
Container fails to start: Check logs withdocker-compose logs mcphub
Rate-limit warning about X-Forwarded-For: Set TRUST_PROXY=1 when MCPHub is behind Nginx, Traefik, a load balancer, or another reverse proxy. Recent versions also default to trusting one proxy automatically inside Docker/Kubernetes, but keeping it explicit in production compose files is recommended.
Database connection errors: Ensure PostgreSQL is healthy and accessible
Port conflicts: Check if ports 3000/5432 are already in use
Volume mount issues: Verify file paths and permissions