> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mcphub.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Docker Setup

> Deploy MCPHub using Docker and Docker Compose

# 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 the `latest-full` image if you need Cargo and the stable Rust toolchain for Rust-based MCP servers.

```bash theme={null}
# Pull the latest image
docker pull samanhappy/mcphub:latest

# Run with default configuration
docker run -d \
  --name mcphub \
  -p 3000:3000 \
  -v ./data:/app/data \
  samanhappy/mcphub:latest
```

The image defaults to `/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

```bash theme={null}
# Clone the repository
git clone https://github.com/your-username/mcphub.git
cd mcphub

# Build the Docker image
docker build -t mcphub:local .

# Run the container
docker run -d \
  --name mcphub \
  -p 3000:3000 \
  -v ./data:/app/data \
  mcphub:local
```

### Building with Extended Features

The Docker image supports an `INSTALL_EXT` build argument to include additional tools. Published `-full` images are built with this enabled:

```bash theme={null}
# Build with extended features (includes stable Rust/Cargo, Docker Engine, Chrome + Firefox for Playwright)
docker build --build-arg INSTALL_EXT=true -t mcphub:extended .

# Option 1: Run with automatic Docker-in-Docker (requires privileged mode)
docker run -d \
  --name mcphub \
  --privileged \
  -p 3000:3000 \
  -v ./data:/app/data \
  mcphub:extended

# Option 2: Run with Docker socket mounted (use host's Docker daemon)
docker run -d \
  --name mcphub \
  -p 3000:3000 \
  -v ./data:/app/data \
  -v /var/run/docker.sock:/var/run/docker.sock \
  mcphub:extended

# Verify Docker is available
docker exec mcphub docker --version
docker exec mcphub docker ps
```

<Note>
  **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

  The extended image is larger but provides additional capabilities for advanced use cases.
</Note>

<Warning>
  **Docker-in-Docker Security Considerations:**

  * **Privileged mode** (`--privileged`): Required for the Docker daemon to start inside the container. This gives the container elevated permissions on the host.
  * **Docker socket mounting** (`/var/run/docker.sock`): Gives the container access to the host's Docker daemon. Both approaches should only be used in trusted environments.
  * For production, consider using Docker socket mounting instead of privileged mode for better security.
</Warning>

## Docker Compose Setup

### Basic Configuration

Create a `docker-compose.yml` file:

```yaml theme={null}
version: '3.8'

services:
  mcphub:
    image: samanhappy/mcphub:latest
    # For local development, use:
    # build: .
    container_name: mcphub
    ports:
      - '3000:3000'
    environment:
      - NODE_ENV=production
      - PORT=3000
      - TRUST_PROXY=1
      - JWT_SECRET=${JWT_SECRET:-your-jwt-secret}
      - DB_URL=postgresql://mcphub:password@postgres:5432/mcphub
    volumes:
      - ./servers.json:/app/servers.json:ro
      - ./data:/app/data
      # Keep installed stdio server packages across container recreates.
      # See "Persisting the stdio package cache" below.
      - mcphub_npm_cache:/root/.npm
      - mcphub_uv_cache:/root/.cache/uv
    depends_on:
      postgres:
        condition: service_healthy
    restart: unless-stopped
    networks:
      - mcphub-network

  postgres:
    image: pgvector/pgvector:pg17
    container_name: mcphub-postgres
    environment:
      - POSTGRES_DB=mcphub
      - POSTGRES_USER=mcphub
      - POSTGRES_PASSWORD=password
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro
    ports:
      - '5432:5432'
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U mcphub -d mcphub']
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped
    networks:
      - mcphub-network

volumes:
  postgres_data:
  mcphub_npm_cache:
  mcphub_uv_cache:

networks:
  mcphub-network:
    driver: bridge
```

### 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:

| Path | Holds |
| - | - |
| `/root/.npm/_npx` | packages installed by `npx` |
| `/root/.npm/_cacache` | npm's downloaded tarballs |
| `/root/.cache/uv` | uv's download cache and the environments `uvx` creates |

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.

<Warning>
  `docker restart` keeps the writable layer, so it does not lose the cache.
  `docker compose down` followed by `up -d` removes and recreates the container, which
  does. If restarting appears to reinstall everything, this distinction is usually why.
</Warning>

Mounting the two paths keeps the installs on the host:

```yaml theme={null}
services:
  mcphub:
    volumes:
      - mcphub_npm_cache:/root/.npm
      - mcphub_uv_cache:/root/.cache/uv

volumes:
  mcphub_npm_cache:
  mcphub_uv_cache:
```

Named volumes survive `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 `stdio` servers occupy roughly 2 GB.
* **To recover from a corrupted install**, use a server's *Reinstall* action — available
  for `npx` and `uvx` servers only. For `uvx` it refreshes just that package, through a
  `--refresh` flag on the next spawn. For `npx` it deletes the whole shared
  `/root/.npm/_npx` directory, so every npx server reinstalls on its next start, not only
  the one you targeted. The `_cacache` volume is what keeps that reinstall cheap.

This matters most at scale. On a deployment with \~95 `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

```yaml theme={null}
version: '3.8'

services:
  nginx:
    image: nginx:alpine
    container_name: mcphub-nginx
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
      - nginx_logs:/var/log/nginx
    depends_on:
      - mcphub
    restart: unless-stopped
    networks:
      - mcphub-network

  mcphub:
    image: samanhappy/mcphub:latest
    container_name: mcphub-app
    expose:
      - '3000'
    environment:
      - NODE_ENV=production
      - PORT=3000
      - TRUST_PROXY=1
      - JWT_SECRET=${JWT_SECRET}
      - DB_URL=postgresql://mcphub:${POSTGRES_PASSWORD}@postgres:5432/mcphub
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - REDIS_URL=redis://redis:6379
    volumes:
      - ./servers.json:/app/servers.json:ro
      - ./data:/app/data
      - mcphub_logs:/app/logs
      - mcphub_npm_cache:/root/.npm
      - mcphub_uv_cache:/root/.cache/uv
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped
    networks:
      - mcphub-network
    healthcheck:
      test: ['CMD', 'wget', '--quiet', '--tries=1', '--spider', 'http://localhost:3000/health']
      interval: 30s
      timeout: 10s
      retries: 3

  postgres:
    image: pgvector/pgvector:pg17
    container_name: mcphub-postgres
    environment:
      - POSTGRES_DB=mcphub
      - POSTGRES_USER=mcphub
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./backups:/backups
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U mcphub -d mcphub']
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped
    networks:
      - mcphub-network

  redis:
    image: redis:7-alpine
    container_name: mcphub-redis
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    healthcheck:
      test: ['CMD', 'redis-cli', 'ping']
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped
    networks:
      - mcphub-network

volumes:
  postgres_data:
  redis_data:
  mcphub_logs:
  mcphub_npm_cache:
  mcphub_uv_cache:
  nginx_logs:

networks:
  mcphub-network:
    driver: bridge
```

### Environment Variables

Create a `.env` file for Docker Compose:

```env theme={null}
# Application
NODE_ENV=production
TRUST_PROXY=1
JWT_SECRET=your-super-secret-jwt-key-change-this

# Database
POSTGRES_PASSWORD=your-secure-database-password

# Redis
REDIS_PASSWORD=your-secure-redis-password

# External APIs
OPENAI_API_KEY=your-openai-api-key

# Optional: Custom port
# PORT=3000
```

## Development Setup

### Development Docker Compose

Create `docker-compose.dev.yml`:

```yaml theme={null}
version: '3.8'

services:
  mcphub-dev:
    build:
      context: .
      dockerfile: Dockerfile.dev
    container_name: mcphub-dev
    ports:
      - '3000:3000'
      - '5173:5173' # Frontend dev server
      - '9229:9229' # Debug port
    environment:
      - NODE_ENV=development
      - PORT=3000
      - DB_URL=postgresql://mcphub:password@postgres:5432/mcphub
    volumes:
      - .:/app
      - /app/node_modules
      - /app/frontend/node_modules
    depends_on:
      - postgres
    command: pnpm dev
    networks:
      - mcphub-dev

  postgres:
    image: pgvector/pgvector:pg17
    container_name: mcphub-postgres-dev
    environment:
      - POSTGRES_DB=mcphub
      - POSTGRES_USER=mcphub
      - POSTGRES_PASSWORD=password
    ports:
      - '5432:5432'
    volumes:
      - postgres_dev_data:/var/lib/postgresql/data
    networks:
      - mcphub-dev

volumes:
  postgres_dev_data:

networks:
  mcphub-dev:
    driver: bridge
```

### Development Dockerfile

Create `Dockerfile.dev`:

```dockerfile theme={null}
FROM node:20-alpine

# Install pnpm
RUN npm install -g pnpm

# Set working directory
WORKDIR /app

# Copy package files
COPY package.json pnpm-lock.yaml ./
COPY frontend/package.json ./frontend/

# Install dependencies
RUN pnpm install

# Copy source code
COPY . .

# Expose ports
EXPOSE 3000 5173 9229

# Start development server
CMD ["pnpm", "dev"]
```

## Running the Application

### Development Mode

```bash theme={null}
# Start development environment
docker-compose -f docker-compose.dev.yml up -d

# View logs
docker-compose -f docker-compose.dev.yml logs -f mcphub-dev

# Stop development environment
docker-compose -f docker-compose.dev.yml down
```

### Production Mode

```bash theme={null}
# Start production environment
docker-compose up -d

# View logs
docker-compose logs -f mcphub

# Stop production environment
docker-compose down
```

## Configuration Management

### MCP Settings Volume Mount

Before the first launch, create `data/mcp_settings.json`:

```json theme={null}
{
  "mcpServers": {
    "fetch": {
      "command": "uvx",
      "args": ["mcp-server-fetch"]
    },
    "playwright": {
      "command": "npx",
      "args": ["@playwright/mcp@latest", "--headless"]
    },
    "amap": {
      "command": "npx",
      "args": ["-y", "@amap/amap-maps-mcp-server"],
      "env": {
        "AMAP_MAPS_API_KEY": "your-api-key"
      }
    }
  }
}
```

### Secrets Management

For production, use Docker secrets:

```yaml theme={null}
version: '3.8'

services:
  mcphub:
    image: samanhappy/mcphub:latest
    environment:
      - JWT_SECRET_FILE=/run/secrets/jwt_secret
      - DATABASE_PASSWORD_FILE=/run/secrets/db_password
    secrets:
      - jwt_secret
      - db_password

secrets:
  jwt_secret:
    file: ./secrets/jwt_secret.txt
  db_password:
    file: ./secrets/db_password.txt
```

## Data Persistence

### Database Backups

Add backup service to your `docker-compose.yml`:

```yaml theme={null}
services:
  backup:
    image: pgvector/pgvector:pg17
    container_name: mcphub-backup
    environment:
      - PGPASSWORD=${POSTGRES_PASSWORD}
    volumes:
      - ./backups:/backups
      - ./scripts/backup.sh:/backup.sh:ro
    command: /bin/sh -c "chmod +x /backup.sh && /backup.sh"
    depends_on:
      - postgres
    profiles:
      - backup
    networks:
      - mcphub-network
```

Create `scripts/backup.sh`:

```bash theme={null}
#!/bin/sh
BACKUP_FILE="/backups/mcphub_$(date +%Y%m%d_%H%M%S).sql"
pg_dump -h postgres -U mcphub -d mcphub > "$BACKUP_FILE"
echo "Backup created: $BACKUP_FILE"

# Keep only last 7 days of backups
find /backups -name "mcphub_*.sql" -mtime +7 -delete
```

Run backup:

```bash theme={null}
docker-compose --profile backup run --rm backup
```

## Monitoring and Health Checks

### Health Check Endpoint

Add to your application:

```javascript theme={null}
// In your Express app
app.get('/health', (req, res) => {
  res.json({
    status: 'healthy',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    memory: process.memoryUsage(),
    version: process.env.npm_package_version,
  });
});
```

### Docker Health Checks

```yaml theme={null}
services:
  mcphub:
    # ... other config
    healthcheck:
      test: ['CMD', 'wget', '--quiet', '--tries=1', '--spider', 'http://localhost:3000/health']
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s
```

### Monitoring with Watchtower

Add automatic updates:

```yaml theme={null}
services:
  watchtower:
    image: containrrr/watchtower
    container_name: mcphub-watchtower
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WATCHTOWER_CLEANUP=true
      - WATCHTOWER_POLL_INTERVAL=3600
      - WATCHTOWER_INCLUDE_STOPPED=true
    restart: unless-stopped
```

## Troubleshooting

### Common Issues

**Container fails to start**: Check logs with `docker-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

### Debug Commands

```bash theme={null}
# Check container status
docker-compose ps

# View logs
docker-compose logs -f [service_name]

# Execute commands in container
docker-compose exec mcphub sh

# Check database connection
docker-compose exec postgres psql -U mcphub -d mcphub

# Restart specific service
docker-compose restart mcphub

# Rebuild and restart
docker-compose up --build -d
```

### Performance Optimization

```yaml theme={null}
services:
  mcphub:
    # ... other config
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '0.5'
        reservations:
          memory: 256M
          cpus: '0.25'
```

This Docker setup provides a complete containerized environment for MCPHub with development and production configurations.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.