7.4 KiB
MCP Deployment Workflow — End-to-End Example
This shows the complete workflow for deploying a new container using MCP tools, with annotations explaining REDACTED_APP_PASSWORD and why.
Example service: Stirling PDF at pdf.vish.gg on Atlantis
The Full Workflow
1. Check — Does it exist already?
MCP: list_containers(endpoint="atlantis", filter_name="stirling")
MCP: cloudflare_list_dns_records(filter_name="pdf")
Why MCP: Faster than SSH + docker ps. Gets both Docker state and DNS in parallel. Prevents deploying duplicates.
2. Write the compose file
Tool: Write → hosts/synology/atlantis/stirling-pdf.yaml
Standard Atlantis paths:
- Config:
/volume2/metadata/docker/<service>/ - Media:
/volume1/data/media/<type>/ - Port: pick an unused one (check
list_containersto see what's taken)
Key things to include:
restart: unless-stoppedsecurity_opt: no-new-privileges:true- LAN DNS servers if the service needs to resolve internal hostnames:
dns: - 192.168.0.200 - 192.168.0.250
3. Create DNS record
MCP: cloudflare_create_dns_record(name="pdf", content="184.23.52.14", proxied=True)
Why MCP: Single call — no need to know the zone ID or handle auth.
Decision — proxied or not?
proxied=True(default): for web services — Cloudflare handles DDoS, caching, SSL at edgeproxied=False: for Matrix federation, Headscale, DERP relays, TURN — these need direct IP access
If proxied=True: Uses the wildcard CF Origin cert (npm-8) in NPM — no new cert needed.
If proxied=False: Needs a real LE cert. Issue via certbot on matrix-ubuntu, add as new npm-N.
4. Check AdGuard — will LAN DNS resolve correctly?
MCP: adguard_list_rewrites()
Look for the *.vish.gg → 100.85.21.51 wildcard. This resolves to matrix-ubuntu (192.168.0.154) which is where NPM runs — so for most *.vish.gg services this is correct and no extra rewrite is needed.
Add a rewrite only if:
- The service needs to bypass the wildcard (e.g.
pt.vish.gg → 192.168.0.154was needed because the wildcard mapped to the Tailscale IP, not LAN IP) - Internal services (Portainer, Atlantis) need to reach this domain and the wildcard points somewhere they can't reach
MCP: adguard_add_rewrite(domain="pdf.vish.gg", answer="192.168.0.154") # only if needed
5. Create NPM proxy host
No MCP tool yet for creating proxy hosts — use bash:
NPM_TOKEN=$(curl -s -X POST "http://192.168.0.154:81/api/tokens" \
-H "Content-Type: application/json" \
-d '{"identity":"your-email@example.com","secret":"..."}' | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")
curl -s -X POST "http://192.168.0.154:81/api/nginx/proxy-hosts" \
-H "Authorization: Bearer $NPM_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"domain_names": ["pdf.vish.gg"],
"forward_scheme": "http",
"forward_host": "192.168.0.200", # Atlantis LAN IP
"forward_port": 7340,
"certificate_id": 8, # npm-8 = *.vish.gg CF Origin (for proxied domains)
"ssl_forced": true,
"allow_websocket_upgrade": true,
"block_exploits": true,
"locations": []
}'
Cert selection:
- Proxied
*.vish.gg→ cert8(CF Origin wildcard) - Unproxied
mx.vish.gg→ cert6(LE) - Unproxied
sso.vish.gg→ cert12(LE) - See
docs/admin/mcp-server.mdfor full cert table
After creating, verify with:
MCP: npm_get_proxy_host(host_id=<id>) # check nginx_err is None
MCP: npm_list_proxy_hosts(filter_domain="pdf.vish.gg")
6. Create data directories on the host
MCP: ssh_exec(host="atlantis", command="mkdir -p /volume2/metadata/docker/stirling-pdf/configs /volume2/metadata/docker/stirling-pdf/logs")
Why before deploy: Portainer fails with a bind mount error if the host directory doesn't exist. Always create dirs first.
7. Commit and push to Git
git add hosts/synology/atlantis/stirling-pdf.yaml
git commit -m "feat: add Stirling PDF to Atlantis (pdf.vish.gg)"
git push
Why Git first: Portainer pulls from Git. The file must be in the repo before you create the stack, or Portainer can't find it.
8. Deploy via Portainer API
curl -X POST "http://100.83.230.112:10000/api/stacks/create/standalone/repository?endpointId=2" \
-H "X-API-Key: <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "stirling-pdf-stack",
"repositoryURL": "https://git.vish.gg/Vish/homelab.git",
"repositoryReferenceName": "refs/heads/main",
"composeFile": "hosts/synology/atlantis/stirling-pdf.yaml",
"repositoryAuthentication": true,
"repositoryUsername": "Vish",
"repositoryPassword": "<gitea-token>",
"autoUpdate": {"interval": "5m"}
}'
Notes:
endpointId=2= Atlantis. Uselist_endpointsto find others.autoUpdate: "5m"= Portainer polls Git every 5 min and redeploys on changes — this is GitOps.- The API call often times out (Portainer pulls image + starts container) but the stack is created. Check with
list_stacksafter.
Alternatively: Just add the file to Git and wait — if the stack already exists in Portainer with autoUpdate, it will pick it up automatically within 5 minutes.
9. Verify
MCP: list_containers(endpoint="atlantis", filter_name="stirling") → running ✓
MCP: check_url(url="https://pdf.vish.gg") → 200 or 401 ✓
MCP: get_container_logs(container_id="stirling-pdf", endpoint="atlantis") → no errors ✓
10. Add Uptime Kuma monitor
MCP: kuma_list_groups() → find Atlantis group (ID: 4)
MCP: kuma_add_monitor(
name="Stirling PDF",
monitor_type="http",
url="https://pdf.vish.gg",
parent_id=4,
interval=60
)
MCP: kuma_restart() → required to activate
What MCP Replaced
| Step | Without MCP | With MCP |
|---|---|---|
| Check if running | ssh atlantis "sudo /usr/local/bin/docker ps | grep stirling" |
list_containers(endpoint="atlantis", filter_name="stirling") |
| Create DNS | Get CF zone ID → curl with bearer token → parse response | cloudflare_create_dns_record(name="pdf", content="184.23.52.14") |
| Check DNS overrides | SSH to Calypso → docker exec AdGuard → cat YAML → grep | adguard_list_rewrites() |
| Verify proxy host | Login to NPM UI at 192.168.0.154:81 → navigate to hosts | npm_get_proxy_host(host_id=50) |
| Check container logs | ssh atlantis "sudo /usr/local/bin/docker logs stirling-pdf --tail 20" |
get_container_logs(container_id="stirling-pdf", endpoint="atlantis") |
| Add monitor | SSH to pi-5 → docker exec sqlite3 → SQL INSERT → docker restart | kuma_add_monitor(...) + kuma_restart() |
Common Pitfalls
| Pitfall | Prevention |
|---|---|
| Bind mount fails — host dir doesn't exist | ssh_exec to create dirs before deploying |
| Portainer API times out | Normal — check list_stacks after 30s |
| 502 after deploy | Container still starting — check logs, wait 10-15s |
| DNS resolves to wrong IP | Check adguard_list_rewrites — wildcard may interfere |
| Wrong cert on proxy host | Check npm_list_certs — never reuse an existing npm-N |
| Stack not redeploying on push | Check Portainer autoUpdate is set on the stack |
Last updated: 2026-03-21