The most lightweight PaaS on the planet. Deploy Docker containers to your VPS from any MCP client. No database, no bloat – just deploy.
Website · Quick Start · MCP Tools · REST API
Demo video: Watch the full demo on the project website.
Run this on any Linux VPS — installs everything in under a minute:
curl -fsSL https://raw.githubusercontent.com/ddalcu/mcp-deploy/main/install.sh | bash- How It Works
- Quick Start
- Usage
- Server
- Reference
┌─────────────────┐ HTTPS ┌─────────────────────────────────────┐
│ Claude Code / │ ────────────────── │ Your VPS │
│ Cursor / any │ MCP Protocol │ │
│ MCP Client │ /mcp │ Traefik ──→ MCP Server (:3000) │
└─────────────────┘ │ │ │
│ ├──→ my-app.deploy.example.com │
┌─────────────────┐ HTTP (direct) │ ├──→ my-api.deploy.example.com │
│ GitHub Actions │ ────────────────── │ └──→ blog.deploy.example.com │
│ / CI/CD / curl │ REST API /api │ │
└─────────────────┘ └─────────────────────────────────────┘
- Traefik handles SSL (automatic per-subdomain Let's Encrypt certs) and routes each subdomain to the right container
- MCP Server receives tool calls over Streamable HTTP and manages Docker containers
- No database — Docker is the source of truth
- No registry required — push images directly or pull from GHCR/DockerHub
You need a Linux VPS with a public IP. Any provider works — if you don't have one yet, Hetzner gives you a $20 credit.
Note: Proxmox/LXC Note: If you want to run this on Proxmox/LXC, make sure its in a VM, not LXC.
Create two DNS A records pointing to your VPS IP:
| Record | Type | Value | Purpose |
|---|---|---|---|
deploy.example.com |
A | YOUR_VPS_IP |
MCP server (mcp.deploy.example.com) |
*.deploy.example.com |
A | YOUR_VPS_IP |
All deployed apps (myapp.deploy.example.com, etc.) |
Note: The wildcard record (
*) must include the dot and asterisk prefix — enter it exactly as*.deploy.example.comin your DNS provider. This is a separate record from the base domain.
Run the installer with curl:
curl -fsSL https://raw.githubusercontent.com/ddalcu/mcp-deploy/main/install.sh | bashOr with wget:
wget -qO- https://raw.githubusercontent.com/ddalcu/mcp-deploy/main/install.sh | bashThe installer asks for two things: your base domain (e.g. deploy.example.com, without * or leading dot) and email (for Let's Encrypt). That's it. It generates an API key and starts everything.
For users who already have Docker and prefer a non-interactive setup:
-
Create the directory and download the compose file:
mkdir -p /opt/mcp-deploy && cd /opt/mcp-deploy curl -fsSL https://raw.githubusercontent.com/ddalcu/mcp-deploy/main/docker-compose.yml -o docker-compose.yml
-
Create a
.envfile:cat > .env <<EOF DOMAIN=deploy.example.com ACME_EMAIL=you@example.com API_KEY=$(openssl rand -hex 32) EOF chmod 600 .env
-
Start the stack:
docker network create web 2>/dev/null; docker compose up -d
Optional: Set
MCP_TRAEFIK=falseandDIRECT_PORT=8080in.envto change how the MCP server is exposed. See Exposure Modes for details.
Copy the config the installer outputs into your MCP client settings.
Claude Code:
claude mcp add deploy https://mcp.deploy.example.com/mcp \
-t http -s user \
-H "Authorization: Bearer YOUR_API_KEY"The -s user flag makes it available in all projects. Restart Claude Code after adding.
Open any project in Claude Code and say:
"Deploy nginx:alpine as hello"
Visit https://hello.deploy.example.com — it's live with SSL.
Build locally and send the image straight to your VPS:
docker build -t my-app:latest .
docker save my-app:latest | curl -X POST -T - \
-H "Authorization: Bearer YOUR_API_KEY" \
https://mcp.deploy.example.com/uploadThen deploy it:
"Deploy my-app:latest as my-app on port 3000"
Use any registry. Public images just work:
"Deploy nginx:alpine as docs on port 80"
For private registries, pass credentials:
"Deploy ghcr.io/myorg/my-app:latest as my-app on port 3000 with registry auth username myuser password ghp_abc123"
Install on as many VPS servers as you want. Add each as a separate MCP server entry:
{
"mcpServers": {
"deploy-prod": {
"type": "streamable-http",
"url": "https://mcp.prod.example.com/mcp",
"headers": { "Authorization": "Bearer PROD_KEY" }
},
"deploy-staging": {
"type": "streamable-http",
"url": "https://mcp.staging.example.com/mcp",
"headers": { "Authorization": "Bearer STAGING_KEY" }
}
}
}Then specify which server: "Deploy my-app to deploy-staging"
Nine tools, available to any MCP client — all managed through natural language.
| Tool | Description |
|---|---|
deploy |
Deploy or redeploy a Docker image with automatic SSL and subdomain routing |
deploy-static |
Deploy a static HTML/CSS/JS site — no Docker image needed |
list |
List all managed deployments |
logs |
Get container logs |
status |
Detailed app info — CPU, memory, uptime, restart count |
stop |
Stop a running app (container preserved) |
start |
Start a stopped app |
remove |
Permanently remove an app and its container |
upload-image |
Push a locally-built Docker image to your VPS — no registry needed |
| Param | Type | Required | Description |
|---|---|---|---|
name |
string | yes | App name — becomes the subdomain (name.domain.com) |
image |
string | yes | Docker image (e.g. nginx:alpine, ghcr.io/user/repo:v1) |
port |
number | no | Port the app listens on inside the container. Auto-detected from the image EXPOSE directive if not provided. |
env |
object | no | Environment variables ({ "KEY": "value" }) |
registry_auth |
object | no | { "username": "...", "password": "..." } for private registries |
volumes |
string[] | no | Named volume mounts (["data:/app/data"]) |
command |
string | no | Override the container's default command |
All MCP tools are also available as standard REST endpoints — no LLM or MCP client required. Use these from GitHub Actions, shell scripts, or any HTTP client.
All endpoints require Authorization: Bearer YOUR_API_KEY.
| Method | Path | Description |
|---|---|---|
POST |
/api/deploy |
Deploy or redeploy a Docker image |
GET |
/api/apps |
List all deployed apps |
GET |
/api/apps/:name |
Get detailed app status |
GET |
/api/apps/:name/logs |
Get app logs |
POST |
/api/apps/:name/stop |
Stop an app |
POST |
/api/apps/:name/start |
Start a stopped app |
DELETE |
/api/apps/:name |
Remove an app |
POST |
/upload |
Upload a Docker image (binary stream) |
POST |
/deploy-static/:name |
Deploy a static site (tar.gz) |
curl -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "my-app", "image": "nginx:alpine", "port": 80}' \
https://mcp.deploy.example.com/api/deployResponse:
{"name": "my-app", "url": "https://my-app.deploy.example.com", "image": "nginx:alpine", "container": "mcp-deploy-my-app", "status": "running"}curl -H "Authorization: Bearer YOUR_API_KEY" \
https://mcp.deploy.example.com/api/apps
# Filter: ?status=running or ?status=stopped# Status
curl -H "Authorization: Bearer $KEY" https://mcp.example.com/api/apps/my-app
# Logs (optional: ?tail=50&since=1h)
curl -H "Authorization: Bearer $KEY" https://mcp.example.com/api/apps/my-app/logs
# Stop
curl -X POST -H "Authorization: Bearer $KEY" https://mcp.example.com/api/apps/my-app/stop
# Start
curl -X POST -H "Authorization: Bearer $KEY" https://mcp.example.com/api/apps/my-app/start
# Remove (optional: ?remove_volumes=true)
curl -X DELETE -H "Authorization: Bearer $KEY" https://mcp.example.com/api/apps/my-appFor CI/CD, you'll typically push images from a registry rather than uploading directly. Recommended registries:
- GitHub Container Registry (GHCR) — free for public repos, integrates with GitHub Actions
- Docker Hub — one free private repo, widely supported
- ttl.sh — anonymous, ephemeral registry (images expire after hours) — great for previews and testing
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/${{ github.repository }}:latest
- name: Deploy to VPS
run: |
curl -fsSL -X POST \
-H "Authorization: Bearer ${{ secrets.DEPLOY_KEY }}" \
-H "Content-Type: application/json" \
-d '{
"name": "my-app",
"image": "ghcr.io/${{ github.repository }}:latest",
"port": 3000,
"registry_auth": {
"username": "${{ github.actor }}",
"password": "${{ secrets.GITHUB_TOKEN }}"
}
}' \
https://mcp.${{ secrets.DEPLOY_DOMAIN }}/api/deployIf you prefer not to use a registry, you can upload the image directly. This requires direct port access for best results.
- name: Build and upload image
run: |
docker build -t my-app:latest .
docker save my-app:latest | curl -fsSL -X POST -T - \
-H "Authorization: Bearer ${{ secrets.DEPLOY_KEY }}" \
http://${{ secrets.VPS_IP }}:8080/upload
- name: Deploy
run: |
curl -fsSL -X POST \
-H "Authorization: Bearer ${{ secrets.DEPLOY_KEY }}" \
-H "Content-Type: application/json" \
-d '{"name": "my-app", "image": "my-app:latest", "port": 3000}' \
http://${{ secrets.VPS_IP }}:8080/api/deploy - name: Deploy static site
run: |
tar -czf site.tar.gz -C ./dist .
curl -fsSL -X POST \
-H "Authorization: Bearer ${{ secrets.DEPLOY_KEY }}" \
-H "Content-Type: application/gzip" \
--data-binary @site.tar.gz \
https://mcp.${{ secrets.DEPLOY_DOMAIN }}/deploy-static/my-siteThe server includes a built-in web dashboard at the root URL (/). It shows all deployed apps with their status, resource usage, and logs — no MCP client required.
- Auth: enter your API key to log in (saved in
localStorage) - App table: name, status (green/red badge), image, URL, CPU, memory
- Logs: click any row to expand and view the last 50 log lines
- Auto-refresh: polls every 30 seconds
- Dark theme, no external dependencies, works on mobile
Access it at https://mcp.deploy.example.com (production) or http://localhost:3000 (local dev).
/opt/mcp-deploy/
├── .env ← your config + API key (chmod 600)
└── docker-compose.yml ← the stack definition
Two files. Everything else runs in Docker:
| Container | Image | Purpose |
|---|---|---|
mcp-deploy-traefik |
traefik:v3.3 |
Reverse proxy + automatic SSL |
mcp-deploy-server |
ghcr.io/ddalcu/mcp-deploy |
MCP server |
Plus a traefik-certs volume for Let's Encrypt certificates and a web network shared by all containers.
The installer asks how the MCP server (dashboard, API, MCP endpoint) should be accessible. This only affects the MCP server — deployed apps always use Traefik for subdomain routing.
| Mode | MCP Server access | Deployed apps |
|---|---|---|
| Domain only (default) | https://mcp.domain.com via Traefik |
Via Traefik |
| Port only | http://IP:PORT direct access |
Via Traefik |
| Both | Domain + direct port | Via Traefik |
Port only is useful when you have your own reverse proxy in front, or only need API/CI access without a subdomain for the MCP server itself.
To change the mode after installation, edit /opt/mcp-deploy/.env:
# Domain only (default) — remove MCP_TRAEFIK and DIRECT_PORT lines
# Port only
MCP_TRAEFIK=false
DIRECT_PORT=8080
# Both
DIRECT_PORT=8080For port only or both, create /opt/mcp-deploy/docker-compose.override.yml:
services:
mcp-server:
ports:
- "${DIRECT_PORT}:3000"Then restart:
cd /opt/mcp-deploy && docker compose up -dSecurity note: The direct port is HTTP (not HTTPS). Use it only for server-to-server communication (CI/CD) where the VPS IP is known. All endpoints still require the API key.
All config is in /opt/mcp-deploy/.env on the VPS:
| Variable | Description | Default |
|---|---|---|
DOMAIN |
Base domain for all services | (required) |
ACME_EMAIL |
Let's Encrypt notification email | (required) |
API_KEY |
Bearer token for MCP server auth | (auto-generated) |
MCP_IMAGE |
Override the MCP server Docker image | ghcr.io/ddalcu/mcp-deploy:latest |
MCP_TRAEFIK |
Route MCP server through Traefik | true |
DIRECT_PORT |
Expose MCP server on this port via IP (no SSL) | (disabled) |
After editing .env, restart the stack:
cd /opt/mcp-deploy && docker compose up -dcd /opt/mcp-deploy && docker compose pull && docker compose up -dThis pulls the latest MCP server image and recreates the container. Your config (.env), deployed apps, and SSL certificates are not affected.
cd /opt/mcp-deploy && docker compose down -v
rm -rf /opt/mcp-deploy
docker network rm web- All traffic is HTTPS via Traefik + Let's Encrypt (automatic per-subdomain certs via HTTP-01 challenge)
- MCP and upload endpoints require
Authorization: Bearer <token>(timing-safe comparison) - API key is generated with
openssl rand -hex 32(256-bit) .envfile ischmod 600(root-only readable)- Volume mounts are restricted to named volumes only (no host path mounts)
- Infrastructure container names are reserved and cannot be overwritten via deploy
- Upload endpoint enforces a 2 GB size limit
graph LR
subgraph Clients
CC[Claude Code / Cursor]
CI[GitHub Actions / curl]
end
subgraph VPS
T[Traefik]
MCP[MCP Server :3000]
D[(Docker)]
T -- "/mcp" --> MCP
T -- "/api/*" --> MCP
MCP -- "manage containers" --> D
T -- "*.domain.com" --> Apps
Apps[Deployed Apps] --> D
end
CC -- "MCP Protocol (HTTPS)" --> T
CI -- "REST API (HTTPS)" --> T
server/src/
├── index.ts ← Express + MCP Streamable HTTP transport + /upload endpoint
├── api.ts ← REST API router (/api/*) for CI/CD
├── auth.ts ← Bearer token middleware (timing-safe)
├── config.ts ← Environment variable config
├── dashboard.ts ← Web dashboard HTML (served at GET /)
├── docker.ts ← Dockerode singleton + pull/load/list helpers
├── labels.ts ← Traefik label generator
├── validation.ts ← Shared app name validation + reserved names
└── tools/
├── deploy.ts ← Pull or use local image, create container with Traefik labels
├── list.ts ← Query Docker for containers labeled mcp-deploy.managed=true
├── logs.ts ← Container log retrieval
├── status.ts ← Container inspect + live CPU/memory stats
├── stop.ts ← Stop container
├── start.ts ← Start container
└── remove.ts ← Stop + remove container
Design principles:
- Docker IS the database — no SQLite, no JSON state files, no sync issues
- Traefik auto-discovers containers via Docker labels — no config reloads
- Each tool is one file (SRP) — easy to add new tools without touching existing code
- Stateless auth — every request validates independently, no sessions
A local dev compose file runs the full stack (Traefik + MCP server) on your machine with self-signed TLS, source mounting, and live reload. Compatible with Docker, Podman, and OrbStack.
# macOS / Linux
echo "127.0.0.1 mcp.mcp.local" | sudo tee -a /etc/hostsFor each app you deploy locally, add its subdomain too (e.g. 127.0.0.1 my-app.mcp.local).
git clone https://github.com/ddalcu/mcp-deploy.git
cd mcp-deploy
docker compose -f docker-compose.dev.yml upThe startup output shows the dashboard URL and API key:
Dashboard: http://localhost:3000
API Key: devkey
| URL | Description |
|---|---|
http://localhost:3000 |
Direct access (bypasses Traefik) |
http://mcp.mcp.local |
Via Traefik (HTTP) |
https://mcp.mcp.local |
Via Traefik (HTTPS, accept self-signed cert warning) |
Source files are bind-mounted from server/src/ — edit and restart the mcp-deploy-server container to pick up changes:
docker restart mcp-deploy-serverOverride defaults with environment variables:
DOMAIN=mydev.local API_KEY=mysecret docker compose -f docker-compose.dev.yml up| Variable | Default | Description |
|---|---|---|
DOMAIN |
mcp.local |
Base domain for routing |
API_KEY |
devkey |
Bearer token for auth |
| MCP Docker servers | Self-hosted PaaS | mcp-deploy | |
|---|---|---|---|
| Remote VPS deployment | No | Yes | Yes |
| MCP protocol (AI-native) | Yes | No | Yes |
| Automatic SSL + subdomains | No | Yes | Yes |
| No database / minimal | N/A | No (most have DBs) | Yes |
| Direct image push | No | Some | Yes |
| REST API for CI/CD | No | Yes | Yes |
MCP Docker servers like mcp-server-docker and docker-mcp let AI manage Docker locally, but don't deploy to remote servers.
Self-hosted PaaS platforms like Coolify, Dokku, CapRover, and Dokploy deploy containers to your VPS with web UIs or CLI, but have no MCP integration.
mcp-deploy combines MCP-native AI control with remote PaaS deployment and zero-database minimalism.
Contributions welcome. Please open an issue first to discuss what you'd like to change.



