Sanitized mirror from private repository - 2026-04-05 12:11:15 UTC
This commit is contained in:
220
docs/admin/mcp-deployment-workflow.md
Normal file
220
docs/admin/mcp-deployment-workflow.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# 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:
|
||||
```yaml
|
||||
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:
|
||||
|
||||
```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
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
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
|
||||
Reference in New Issue
Block a user