Docker image to run a self-hosted MCP (Model Context Protocol) gateway with Bearer token auth. Multi-server hub powered by MCPHub + Caddy, access filesystem, fetch, GitHub, Brave Search, Git, PostgreSQL, memory, and more from a single HTTP endpoint. Simple env-file config, multi-arch (amd64, arm64).
English | 简体中文 | 繁體中文 | Русский
MCP Gateway on Docker
Docker image to run a self-hosted MCP (Model Context Protocol) gateway, providing authenticated access to multiple MCP tool servers over HTTP from a single endpoint. Powered by MCPHub with Caddy auth proxy. Designed to be simple and secure by default.
Features:
- Secure by default — all API requests require a Bearer token (auto-generated on first start)
- Auto-generates an API key on first start, stored in the persistent volume
- Multi-server gateway — run multiple MCP tool servers behind a single HTTP endpoint
- Path-based routing — access all servers at
/mcpor individual servers at/mcp/<name> - Streamable HTTP + SSE — both MCP transport modes supported
- Dashboard — web UI at
/for monitoring MCP server status - Env-file configuration — simple
mcp.envfile; no JSON editing - Built-in MCP servers: filesystem, fetch, GitHub, Brave Search, Git, PostgreSQL, memory, sequential-thinking
- Caddy reverse proxy enforces Bearer token auth on all API requests (except
/healthhealth check) - Works with LiteLLM to give any LLM access to MCP tools
- Automatically built and published via GitHub Actions
- Persistent configuration via a Docker volume
- Multi-arch:
linux/amd64,linux/arm64
Also available:
- AI/Audio: Whisper (STT), Kokoro (TTS), Embeddings, LiteLLM, Ollama (LLM)
- VPN: WireGuard, OpenVPN, IPsec VPN, Headscale
Tip: MCP Gateway, Ollama, LiteLLM, Whisper, Kokoro, and Embeddings can be used together to build a complete, private AI stack on your own server — with tool access, local LLMs, voice I/O, and semantic search.
Security note
MCP servers have no built-in authentication. Exposing them publicly without auth is the same class of problem as the ~175,000 unauthenticated Ollama servers found publicly exposed (source). This image enforces Bearer token authentication on all API requests via a built-in Caddy auth proxy, so unauthorized access is blocked even if the port is accidentally exposed.
Quick start
Step 1. Start the MCP Gateway:
docker run \
--name mcp-gateway \
--restart=always \
-v mcp-data:/var/lib/mcp \
-p 3000:3000/tcp \
-d hwdsl2/mcp-gateway
On first start, an API key is auto-generated and displayed in the container logs. All API requests require this key.
Note: For internet-facing deployments with HTTPS, see Using a reverse proxy.
Step 2. Get the API key:
# View the key in the container logs
docker logs mcp-gateway
# Or retrieve it for use in scripts
MCP_KEY=$(docker exec mcp-gateway mcp_manage --getkey)
The API key is displayed in a box labeled MCP Gateway API key. To display it again at any time:
docker exec mcp-gateway mcp_manage --showkey
Step 3. Test with the API:
MCP_KEY=$(docker exec mcp-gateway mcp_manage --getkey)
# Test the MCP endpoint (fetch server is enabled by default)
curl http://localhost:3000/mcp \
-H "Authorization: Bearer $MCP_KEY"
# Check gateway health (no auth required)
curl http://localhost:3000/health
Note: The docker exec management commands (mcp_manage) do not require the API key.
To learn more about how to use this image, read the sections below.
Requirements
- A Linux server (local or cloud) with Docker installed
- At least 512 MB of available RAM
- TCP port 3000 (or your configured port) accessible
Download
Get the trusted build from the Docker Hub registry:
docker pull hwdsl2/mcp-gateway
Alternatively, you may download from Quay.io:
docker pull quay.io/hwdsl2/mcp-gateway
docker image tag quay.io/hwdsl2/mcp-gateway hwdsl2/mcp-gateway
Supported platforms: linux/amd64 and linux/arm64.
Environment variables
All variables are optional. If not set, secure defaults are used automatically.
This Docker image uses the following variables, that can be declared in an env file (see example):
| Variable | Description | Default |
|---|---|---|
| MCP_API_KEY | API key for authenticating requests (auto-generated if not set) | Auto-generated |
| MCP_PORT | TCP port for the gateway (1–65535) | 3000 |
| MCP_HOST | Hostname or IP shown in startup info and --showkey output | Auto-detected |
| MCP_SERVERS | Comma-separated list of MCP servers to enable | fetch |
| MCP_ADMIN_PASSWORD | Password for the MCPHub dashboard admin account (auto-generated on first start if not set) | Auto-generated |
Note: In your env file, you may enclose values in single quotes, e.g. VAR='value'. Do not add spaces around =. If you change MCP_PORT, update the -p flag in the docker run command accordingly.
Example using an env file:
cp mcp.env.example mcp.env
# Edit mcp.env and set your values, then:
docker run \
--name mcp-gateway \
--restart=always \
-v mcp-data:/var/lib/mcp \
-v ./mcp.env:/mcp.env:ro \
-p 3000:3000/tcp \
-d hwdsl2/mcp-gateway
Available MCP servers
Enable servers by listing them in MCP_SERVERS (comma-separated):
| Server | Required config | Description |
|---|---|---|
| fetch | — | Fetch URLs and extract content |
| filesystem | MCP_FILESYSTEM_DIRS | Read/write files in allowed directories |
| github | MCP_GITHUB_TOKEN | GitHub API access (repos, issues, PRs) |
| brave-search | MCP_BRAVE_API_KEY | Web search via Brave Search API |
| git | MCP_GIT_REPO | Git repository tools (status, diff, commit, log) |
| postgres | MCP_POSTGRES_URL | Query PostgreSQL databases |
| memory | — | Knowledge graph / persistent memory |
| sequential-thinking | — | Structured thinking and reasoning |
Example:
# Enable filesystem, fetch, and GitHub servers
MCP_SERVERS=filesystem,fetch,github
MCP_FILESYSTEM_DIRS=/data/docs,/data/projects
MCP_GITHUB_TOKEN=ghp_your_token_here
For the filesystem server, bind-mount host directories into the container:
docker run \
--name mcp-gateway \
--restart=always \
-v mcp-data:/var/lib/mcp \
-v ./mcp.env:/mcp.env:ro \
-v /home/user/documents:/data/docs:ro \
-v /home/user/projects:/data/projects \
-p 3000:3000/tcp \
-d hwdsl2/mcp-gateway
For the git server, bind-mount the repository into the container and set MCP_GIT_REPO:
MCP_SERVERS=git
MCP_GIT_REPO=/repo
docker run \
--name mcp-gateway \
--restart=always \
-v mcp-data:/var/lib/mcp \
-v ./mcp.env:/mcp.env:ro \
-v /home/user/myrepo:/repo \
-p 3000:3000/tcp \
-d hwdsl2/mcp-gateway
Managing MCP servers
Use docker exec to manage the gateway with the mcp_manage helper script.
List enabled servers:
docker exec mcp-gateway mcp_manage --list
Test a specific server:
docker exec mcp-gateway mcp_manage --test fetch
docker exec mcp-gateway mcp_manage --test github
Show gateway status:
docker exec mcp-gateway mcp_manage --status
Show the API key:
docker exec mcp-gateway mcp_manage --showkey
Get the API key (machine-readable, for use in scripts):
MCP_KEY=$(docker exec mcp-gateway mcp_manage --getkey)
Add or remove servers at runtime:
Use the MCPHub dashboard at http://<server>:3000/ to add, configure, or remove MCP servers without restarting the container. Changes are saved to the persistent volume and survive restarts.
Note:
MCP_SERVERSonly applies on the first run whenmcp_settings.jsonis created. After that, the dashboard is the way to manage servers. To re-applyMCP_SERVERSfrom scratch, remove the config file and restart:docker exec mcp-gateway rm /var/lib/mcp/mcp_settings.json docker restart mcp-gateway
Using the API
All API requests require a Bearer token. Retrieve the API key first:
MCP_KEY=$(docker exec mcp-gateway mcp_manage --getkey)
MCP endpoint (all enabled servers):
curl http://localhost:3000/mcp \
-H "Authorization: Bearer $MCP_KEY"
MCP endpoint (specific server):
curl http://localhost:3000/mcp/fetch \
-H "Authorization: Bearer $MCP_KEY"
Dashboard (web UI):
Open http://localhost:3000/ in a browser with Authorization: Bearer <key>, or use a client that supports header injection.
Health check (no auth required):
curl http://localhost:3000/health
Connecting AI clients
Cline (VS Code) — in Cline's MCP settings:
{
"mcpServers": {
"gateway": {
"url": "http://localhost:3000/mcp",
"transport": "sse",
"headers": {
"Authorization": "Bearer <api_key>"
}
}
}
}
Claude Desktop — in claude_desktop_config.json:
{
"mcpServers": {
"gateway": {
"url": "http://localhost:3000/mcp",
"transport": "streamable-http",
"headers": {
"Authorization": "Bearer <api_key>"
}
}
}
}
Persistent data
All gateway data is stored in the Docker volume (/var/lib/mcp inside the container):
/var/lib/mcp/
├── mcp_settings.json # Generated MCPHub configuration
├── .api_key # API key (auto-generated, or synced from MCP_API_KEY)
├── .initialized # First-run marker
├── .port # Saved port (used by mcp_manage)
├── .server_addr # Cached server address (used by mcp_manage --showkey)
├── .servers # Enabled servers list (used by mcp_manage)
└── .Caddyfile # Generated Caddy config (auth proxy)
mcp_settings.json is generated from MCP_SERVERS on first run only. Subsequent restarts reuse the existing file, preserving any changes made via the dashboard.
Back up the Docker volume to preserve your configuration and API key.
Using docker-compose
cp mcp.env.example mcp.env
# Edit mcp.env and set your values, then:
docker compose up -d
docker logs mcp-gateway
Example docker-compose.yml (already included):
services:
mcp-gateway:
image: hwdsl2/mcp-gateway
container_name: mcp-gateway
restart: always
ports:
- "3000:3000/tcp"
volumes:
- mcp-data:/var/lib/mcp
- ./mcp.env:/mcp.env:ro
# Mount host directories for the filesystem MCP server (optional):
# - /path/to/docs:/data/docs:ro
# - /path/to/code:/data/code:ro
volumes:
mcp-data:
Using a reverse proxy
For internet-facing deployments, put a reverse proxy in front to handle HTTPS. The built-in Caddy auth proxy handles authentication; the external reverse proxy adds TLS. Use one of the following addresses to reach the MCP Gateway container:
mcp-gateway:3000— if your reverse proxy runs as a container in the same Docker network.127.0.0.1:3000— if your reverse proxy runs on the host and the port is published.
Note: The Authorization: Bearer header passes through reverse proxies automatically — no special configuration needed.
Example with Caddy (automatic TLS via Let's Encrypt):
Caddyfile:
mcp.example.com {
reverse_proxy mcp-gateway:3000
}
Example with nginx (reverse proxy on the host):
server {
listen 443 ssl;
server_name mcp.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
After setting up a reverse proxy, set MCP_HOST=mcp.example.com in your env file so that the correct endpoint URL is shown in the startup logs and mcp_manage --showkey output.
Update Docker image
To update the Docker image and container, first download the latest version:
docker pull hwdsl2/mcp-gateway
If the Docker image is already up to date, you should see:
Status: Image is up to date for hwdsl2/mcp-gateway:latest
Otherwise, it will download the latest version. Remove and re-create the container:
docker rm -f mcp-gateway
# Then re-run the docker run command from Quick start with the same volume.
Your configuration and API key are preserved in the mcp-data volume.
Using with other AI services
The MCP Gateway, Ollama (LLM), LiteLLM, Whisper (STT), Kokoro (TTS), and Embeddings images can be combined to build a complete, private AI stack on your own server — from voice I/O to RAG-powered question answering. MCP Gateway provides tools (file access, web search, GitHub, databases) to any LLM client that supports MCP. Whisper, Kokoro, and Embeddings run fully locally. Ollama runs all LLM inference locally, so no data is sent to third parties. When using LiteLLM with external providers (e.g., OpenAI, Anthropic), your data will be sent to those providers.
graph LR
A["🤖 AI Client<br/>(Cline, Claude)"] -->|MCP tools| G["MCP Gateway<br/>(tool access)"]
G -->|filesystem| F["📁 Files"]
G -->|fetch/search| W["🌐 Web"]
G -->|github| GH["🐙 GitHub"]
A -->|chat| L["LiteLLM<br/>(AI gateway)"]
L -->|routes to| O["Ollama<br/>(local LLM)"]
L -->|MCP tools| G
graph LR
D["📄 Documents"] -->|embed| E["Embeddings<br/>(text → vectors)"]
E -->|store| VDB["Vector DB<br/>(Qdrant, Chroma)"]
A["🎤 Audio input"] -->|transcribe| W["Whisper<br/>(speech-to-text)"]
W -->|query| E
VDB -->|context| L["LiteLLM<br/>(AI gateway)"]
W -->|text| L
L -->|routes to| O["Ollama<br/>(local LLM)"]
L -->|response| T["Kokoro TTS<br/>(text-to-speech)"]
T --> B["🔊 Audio output"]
L -->|MCP protocol| M["MCP Gateway<br/>(MCP endpoint)"]
M --> C["🤖 AI assistant<br/>(Claude, Cursor, etc.)"]
| Service | Role | Default port |
|---|---|---|
| Ollama (LLM) | Runs local LLM models (llama3, qwen, mistral, etc.) | 11434 |
| LiteLLM | AI gateway — routes requests to Ollama, OpenAI, Anthropic, and 100+ providers | 4000 |
| Embeddings | Converts text to vectors for semantic search and RAG | 8000 |
| Whisper (STT) | Transcribes spoken audio to text | 9000 |
| Kokoro (TTS) | Converts text to natural-sounding speech | 8880 |
| MCP Gateway | Provides MCP tools (filesystem, fetch, GitHub, search, databases) to AI clients | 3000 |
Connect MCP Gateway to LiteLLM:
# In your LiteLLM config, add the MCP gateway as a tool source:
mcp_servers:
- url: http://mcp-gateway:3000/mcp
transport: sse
headers:
Authorization: "Bearer <mcp_api_key>"
MCP tools example
Use MCP Gateway to give your AI assistant access to files, web, and GitHub:
MCP_KEY=$(docker exec mcp mcp_manage --getkey)
# Use MCP endpoint with an AI client (e.g., Cline in VS Code)
# Set the MCP server URL: http://localhost:3000/mcp
# Set Authorization header: Bearer <api_key>
# Or test the MCP endpoint directly with an initialize request
curl -s http://localhost:3000/mcp \
-X POST \
-H "Authorization: Bearer $MCP_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
Voice pipeline example
Transcribe a spoken question, get a local LLM response via Ollama, and convert it to speech:
LITELLM_KEY=$(docker exec litellm litellm_manage --getkey)
# Step 1: Transcribe audio to text (Whisper)
TEXT=$(curl -s http://localhost:9000/v1/audio/transcriptions \
-F file=@question.mp3 -F model=whisper-1 | jq -r .text)
# Step 2: Send text to Ollama via LiteLLM and get a response
RESPONSE=$(curl -s http://localhost:4000/v1/chat/completions \
-H "Authorization: Bearer $LITELLM_KEY" \
-H "Content-Type: application/json" \
-d "{\"model\":\"ollama/llama3.2:3b\",\"messages\":[{\"role\":\"user\",\"content\":\"$TEXT\"}]}" \
| jq -r '.choices[0].message.content')
# Step 3: Convert the response to speech (Kokoro TTS)
curl -s http://localhost:8880/v1/audio/speech \
-H "Content-Type: application/json" \
-d "{\"model\":\"tts-1\",\"input\":\"$RESPONSE\",\"voice\":\"af_heart\"}" \
--output response.mp3
RAG pipeline example
Embed documents for semantic search, retrieve context, then answer questions with a local Ollama model:
LITELLM_KEY=$(docker exec litellm litellm_manage --getkey)
# Step 1: Embed a document chunk and store the vector in your vector DB
curl -s http://localhost:8000/v1/embeddings \
-H "Content-Type: application/json" \
-d '{"input": "Docker simplifies deployment by packaging apps in containers.", "model": "text-embedding-ada-002"}' \
| jq '.data[0].embedding'
# → Store the returned vector alongside the source text in Qdrant, Chroma, pgvector, etc.
# Step 2: At query time, embed the question, retrieve the top matching chunks from
# the vector DB, then send the question and retrieved context to Ollama via LiteLLM.
curl -s http://localhost:4000/v1/chat/completions \
-H "Authorization: Bearer $LITELLM_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "ollama/llama3.2:3b",
"messages": [
{"role": "system", "content": "Answer using only the provided context."},
{"role": "user", "content": "What does Docker do?\n\nContext: Docker simplifies deployment by packaging apps in containers."}
]
}' \
| jq -r '.choices[0].message.content'
Full stack docker-compose example
Deploy all services with a single command. No setup required — all services auto-configure with secure defaults on first start.
Resource requirements: Running all services together requires at least 8 GB of RAM (with small models). For larger LLM models (8B+), 32 GB or more is recommended. You can comment out services you don't need to reduce memory usage.
services:
ollama:
image: hwdsl2/ollama-server
container_name: ollama
restart: always
# ports:
# - "11434:11434/tcp" # Uncomment for direct access to Ollama
volumes:
- ollama-data:/var/lib/ollama
# - ./ollama.env:/ollama.env:ro # optional: custom config
litellm:
image: hwdsl2/litellm-server
container_name: litellm
restart: always
ports:
- "4000:4000/tcp"
environment:
- LITELLM_OLLAMA_BASE_URL=http://ollama:11434
volumes:
- litellm-data:/etc/litellm
# - ./litellm.env:/litellm.env:ro # optional: custom config
embeddings:
image: hwdsl2/embeddings-server
container_name: embeddings
restart: always
ports:
- "8000:8000/tcp"
volumes:
- embeddings-data:/var/lib/embeddings
# - ./embed.env:/embed.env:ro # optional: custom config
whisper:
image: hwdsl2/whisper-server
container_name: whisper
restart: always
ports:
- "9000:9000/tcp"
volumes:
- whisper-data:/var/lib/whisper
# - ./whisper.env:/whisper.env:ro # optional: custom config
kokoro:
image: hwdsl2/kokoro-server
container_name: kokoro
restart: always
ports:
- "8880:8880/tcp"
volumes:
- kokoro-data:/var/lib/kokoro
# - ./kokoro.env:/kokoro.env:ro # optional: custom config
mcp:
image: hwdsl2/mcp-gateway
container_name: mcp
restart: always
ports:
- "3000:3000/tcp"
volumes:
- mcp-data:/var/lib/mcp
# - ./mcp.env:/mcp.env:ro # optional: custom config
volumes:
ollama-data:
litellm-data:
embeddings-data:
whisper-data:
kokoro-data:
mcp-data:
For NVIDIA GPU acceleration, change image tags to :cuda for ollama, whisper, and kokoro, and add the following to each of those services:
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
Technical details
- Base image:
samanhappy/mcphub(Python 3.13 + Node.js 22) - Auth proxy: Caddy (always active, enforces Bearer token auth)
- Gateway: MCPHub (multi-server MCP hub)
- MCPHub internal port:
3001(not published; Caddy proxies fromMCP_PORT) - Data directory:
/var/lib/mcp(Docker volume) - Gateway API:
http://localhost:3000(or your configured port) - MCP endpoint:
http://localhost:3000/mcp - Multi-arch:
linux/amd64,linux/arm64
License
Note: The software components inside the pre-built image (such as MCPHub, Caddy, and their dependencies) are under the respective licenses chosen by their respective copyright holders. As for any pre-built image usage, it is the image user's responsibility to ensure that any use of this image complies with any relevant licenses for all software contained within.
Copyright (C) 2026 Lin Song
This work is licensed under the MIT License.
MCPHub is Copyright (C) 2025 samanhappy, and is distributed under the Apache License 2.0.
Caddy is Copyright (C) 2015 Matthew Holt and The Caddy Authors, and is distributed under the Apache License 2.0.
This project is an independent Docker setup for MCPHub and is not affiliated with, endorsed by, or sponsored by MCPHub.