Deployment
Docker Compose
Section titled “Docker Compose”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"]docker compose up -d # startdocker compose logs -f # follow logsdocker compose ps # shows "healthy" once the health check passesdocker compose down # stopThe 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.pemCopy the file from any node with scp root@pve1:/etc/pve/pve-root-ca.pem ..
Image tags
Section titled “Image tags”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:
gh attestation verify oci://ghcr.io/mattoddie/proxmox-mcp:1.4.2 --owner mattoddieUpgrading
Section titled “Upgrading”docker compose pulldocker compose up -dCheck 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.
Building from source
Section titled “Building from source”git clone https://github.com/mattoddie/proxmox-mcp.gitcd proxmox-mcpdocker 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.
TLS with a reverse proxy
Section titled “TLS with a reverse proxy”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=8080When 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.
Hardening
Section titled “Hardening”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.
Running on the cluster itself
Section titled “Running on the cluster itself”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_URLat 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.
Running without Docker
Section titled “Running without Docker”Docker is the supported way to run proxmox-mcp, but the server is an ordinary Node.js 22+ program:
git clone https://github.com/mattoddie/proxmox-mcp.gitcd proxmox-mcpnpm ci && npm run buildPROXMOX_URL=https://pve1:8006 PROXMOX_API_TOKEN='mcp@pve!mcp=...' node dist/index.js # stdio by defaultMCP_TRANSPORT=http PROXMOX_URL=... PROXMOX_API_TOKEN=... node dist/index.js