Skip to content

Deployment

The repository’s compose.yaml is a good starting point:

services:
proxmox-mcp:
image: ghcr.io/mattoddie/proxmox-mcp:latest
container_name: proxmox-mcp
restart: unless-stopped
env_file: .env
ports:
- "8080:8080"
read_only: true
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
Terminal window
docker compose up -d # start
docker compose logs -f # follow logs
docker compose ps # shows "healthy" once the health check passes
docker compose down # stop

The image has a built-in health check that calls /health every 30 seconds. It only checks that the server is running, not that Proxmox is reachable.

To listen on one interface only, such as a LAN address or localhost when you use a reverse proxy, change the port mapping to "192.168.1.20:8080:8080" or "127.0.0.1:8080:8080".

To verify Proxmox’s certificate with the cluster CA, mount it read-only and point PROXMOX_CA_FILE at it:

volumes:
- ./pve-root-ca.pem:/certs/pve-root-ca.pem:ro
environment:
PROXMOX_VERIFY_SSL: "true"
PROXMOX_CA_FILE: /certs/pve-root-ca.pem

Copy the file from any node with scp root@pve1:/etc/pve/pve-root-ca.pem ..

Images are published to the GitHub Container Registry at ghcr.io/mattoddie/proxmox-mcp for linux/amd64 and linux/arm64. Each GitHub release publishes these tags:

Tag Example Moves?
X.Y.Z 1.4.2 Never. Pin this for full reproducibility.
X.Y 1.4 Moves to the latest patch release (bug fixes).
X 1 Moves to the latest minor release (new features, no breaking changes). Not published for 0.x.
latest Moves to the newest stable release.

Pre-releases, such as 1.5.0-rc.1, are only published under their exact version, never as latest.

Every image includes an SBOM and build provenance attestation. To verify that an image was built by this repository’s release workflow:

Terminal window
gh attestation verify oci://ghcr.io/mattoddie/proxmox-mcp:1.4.2 --owner mattoddie
Terminal window
docker compose pull
docker compose up -d

Check the changelog or the release notes before upgrading across a major version. After an upgrade, reconnect or restart your MCP client so it fetches the new tool list.

Terminal window
git clone https://github.com/mattoddie/proxmox-mcp.git
cd proxmox-mcp
docker build -t proxmox-mcp .

The tests run as part of the image build, so a broken build never produces an image. To use a local build with Compose, replace image: in compose.yaml with build: . and run docker compose up -d --build.

The server speaks plain HTTP. If it’s reachable from outside a trusted network, put a TLS-terminating reverse proxy in front of it and keep MCP_AUTH_TOKEN set.

Caddy, which gets certificates automatically:

proxmox-mcp.example.com {
reverse_proxy proxmox-mcp:8080
}

nginx:

server {
listen 443 ssl;
server_name proxmox-mcp.example.com;
ssl_certificate /etc/ssl/certs/proxmox-mcp.pem;
ssl_certificate_key /etc/ssl/private/proxmox-mcp.key;
location /mcp {
proxy_pass http://proxmox-mcp:8080;
proxy_set_header Host $host;
proxy_read_timeout 600s; # tools wait for Proxmox tasks (PROXMOX_TASK_TIMEOUT_MS)
}
}

Traefik labels:

services:
proxmox-mcp:
image: ghcr.io/mattoddie/proxmox-mcp:latest
env_file: .env
labels:
- traefik.enable=true
- traefik.http.routers.proxmox-mcp.rule=Host(`proxmox-mcp.example.com`)
- traefik.http.routers.proxmox-mcp.tls.certresolver=letsencrypt
- traefik.http.services.proxmox-mcp.loadbalancer.server.port=8080

When the server sits behind a proxy, don’t publish its port. Set MCP_ALLOWED_HOSTS=proxmox-mcp.example.com so it only accepts requests for that hostname.

Tools that start a Proxmox task wait for it for up to PROXMOX_TASK_TIMEOUT_MS (two minutes by default), and some, such as guest agent commands, have their own timeouts. Make sure the proxy’s read timeout is longer than the longest wait you’ve configured, or the client sees a gateway timeout while the task is still running.

The image already runs as the unprivileged node user and doesn’t need a writable filesystem, and the default compose.yaml already sets read_only, cap_drop: [ALL] and no-new-privileges. You can lock it down further:

services:
proxmox-mcp:
image: ghcr.io/mattoddie/proxmox-mcp:1.4.2 # pinned
read_only: true
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
mem_limit: 256m
pids_limit: 100
env_file: .env
ports: ["127.0.0.1:8080:8080"]

On the Proxmox side, the most effective hardening is a narrowly scoped API token. See Security for choosing permissions and toolsets.

It’s common to run proxmox-mcp in a small VM or LXC container on the cluster it manages. That works, with two things to keep in mind:

  • If the assistant can stop or delete guests, it can stop or delete the one proxmox-mcp runs in. Turn on Protection in that guest’s options to block deleting it, and to block stopping it too, keep it out of the token’s reach: put the guests the assistant should manage in a pool and grant the token rights on that pool rather than on /vms.
  • Point PROXMOX_URL at a node, or a load-balanced name, that stays up while the guest migrates.

Running Docker directly on a Proxmox node is possible but not recommended by Proxmox. Use a VM or container instead.

Docker is the supported way to run proxmox-mcp, but the server is an ordinary Node.js 22+ program:

Terminal window
git clone https://github.com/mattoddie/proxmox-mcp.git
cd proxmox-mcp
npm ci && npm run build
PROXMOX_URL=https://pve1:8006 PROXMOX_API_TOKEN='mcp@pve!mcp=...' node dist/index.js # stdio by default
MCP_TRANSPORT=http PROXMOX_URL=... PROXMOX_API_TOKEN=... node dist/index.js