Files
homelab-optimized/docs/admin/mcp-deployment-workflow.md
Gitea Mirror Bot 5c2fcfeb21
Some checks failed
Documentation / Build Docusaurus (push) Failing after 5m1s
Documentation / Deploy to GitHub Pages (push) Has been skipped
Sanitized mirror from private repository - 2026-03-28 12:26:38 UTC
2026-03-28 12:26:38 +00:00

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_containers to see what's taken)

Key things to include:

  • restart: unless-stopped
  • security_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 edge
  • proxied=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.154 was 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 → cert 8 (CF Origin wildcard)
  • Unproxied mx.vish.gg → cert 6 (LE)
  • Unproxied sso.vish.gg → cert 12 (LE)
  • See docs/admin/mcp-server.md for 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. Use list_endpoints to 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_stacks after.

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