diff --git a/skills/saiop-infrastructure/saiop-webapp-behind-traefik/SKILL.md b/skills/saiop-infrastructure/saiop-webapp-behind-traefik/SKILL.md new file mode 100644 index 0000000..094deea --- /dev/null +++ b/skills/saiop-infrastructure/saiop-webapp-behind-traefik/SKILL.md @@ -0,0 +1,86 @@ +--- +name: saiop-webapp-behind-traefik +description: "Deploy a self-hosted web app on the SAIOP stack behind Traefik + Cloudflare, avoiding the recurring pitfalls hit deploying Portainer, NetBox/Neo4j, and Obsidian." +version: 1.0.0 +author: Claude Code (SAIOP ops session) +license: MIT +platforms: [linux] +prerequisites: + env_vars: [] + commands: [docker, curl, dig] +metadata: + hermes: + tags: [SAIOP, Traefik, Cloudflare, Docker, deployment, infrastructure] +--- + +# Deploying a web app behind Traefik + Cloudflare on SAIOP + +Checklist distilled from real incidents during the Portainer, NetBox/Neo4j, and +Obsidian deployments on this stack. Each item below caused a real outage or a +multi-hour debugging session the first time; check all of them up front. + +## When to use + +Any time a new container needs a public `*.atu.edu.gh` hostname through the +existing Traefik + Cloudflare setup on the `saiops` VM. + +## Checklist + +1. **Never publish a host port directly** (`ports: ["8080:8080"]` on the app + container). Docker's own iptables rules for published ports bypass UFW + entirely — confirmed twice (Portainer, Gitea's SSH port). Always route + through Traefik labels instead; no `ports:` block on the app service. + +2. **Check for pre-provisioned secrets/DB before building from scratch.** + This stack's `.env.master` and shared Postgres instance often already have + a database/role and secrets created for a planned-but-not-yet-deployed + service (confirmed for NetBox, n8n). `grep` `.env.master` for the + service's name and `\l` the shared Postgres instance first. + +3. **Check container UID before mounting host directories.** Don't assume + `aiadmin`'s UID. `docker run --rm --entrypoint id ` first. Official + Prometheus-ecosystem images often run as `nobody` (65534); LinuxServer.io + images use `PUID`/`PGID` env vars — set them to match the actual owning + user (this stack: `aiadmin` = 1001), not the LSIO default. + +4. **Raw TCP protocols (anything not plain HTTP/S) cannot go through + Cloudflare's free proxy.** If a service needs a non-HTTP port (e.g. Neo4j + Bolt), either: (a) keep that one DNS record DNS-only and the main + hostname proxied (split-hostname pattern — see NetBox/Neo4j skill), or + (b) don't expose it publicly at all if it's only needed internally. + +5. **Switching a previously-proxied hostname to DNS-only removes Cloudflare's + CDN caching for that whole hostname, not just whatever you needed + unblocked.** If large static assets (JS/CSS bundles) suddenly take 10-20s + to load after such a switch, that's why — it's not a Traefik/app bug. + Either keep the asset-serving hostname proxied and split out only the + raw-protocol piece to a second hostname, or override `Cache-Control` via + a Traefik `customResponseHeaders` middleware so Cloudflare can still cache + despite an app-set `no-store` header. + +6. **Self-hosted web-app bundles sometimes ship a CSP too strict for their + own JS** (seen with Neo4j Browser needing `unsafe-eval`). If a page loads + but renders blank with no obvious network error, check the browser + console for CSP violations before assuming a deeper bug. + +7. **Browsers require encrypted WebSocket (`wss://`) from an HTTPS page** — + mixed-content policy blocks plaintext `ws://` even to a different host. + If a backend protocol needs browser access over WebSocket (Neo4j Bolt, + etc.), terminate TLS on that port at Traefik too (`tls=true` + + `certresolver=letsencrypt` on the TCP router), not just the main HTTP + route. + +8. **When debugging "works on the server, fails for the client," isolate by + distance before assuming the server is wrong:** test container-direct → + through-proxy-on-localhost → through-proxy-externally. Every step here + should be proven correct before trusting the next. Don't skip straight to + blaming the client/network — but don't keep patching server config either + once every layer up to "through-proxy-externally" has been proven clean. + +## Don't do this + +- Don't add a host port publish "just to test" — it's the single most common + way a new service silently bypasses the firewall on this box. +- Don't assume a slow page load is a config bug before checking your own + test connection's raw bandwidth against a known-good CDN endpoint + (`curl https://speed.cloudflare.com/__down?bytes=2000000`).