Infrastructure Overview
Network topology
Two VPS nodes connected via Tailscale mesh VPN. All inter-VPS traffic uses Tailscale IPs. Neither server exposes SSH or application ports publicly.
Internet → Cloudflare (CDN, DDoS, WAF) → Cloudflare Access (Zero Trust identity perimeter) → cloudflared tunnel (QUIC, web VPS) → level147-net:3000 (Next.js ops dashboard)
Gitea (tools VPS) → Woodpecker CI → Docker build + push to Gitea registry → SSH deploy to web VPS via TailscaleServer roster
| Host | Tailscale IP | Role |
|---|---|---|
| Web VPS | 100.101.148.89 | Ops dashboard, cloudflared tunnel |
| Tools VPS | 100.81.122.65 | Gitea, Woodpecker CI, Portainer |
| Apps host | 100.83.125.55 | Application services (widget data sources) |
Web VPS — container stack
| Container | Image | Network | Purpose |
|---|---|---|---|
level147-net | gitea.level147.net/level147/level147.net:prod | proxy-net | Ops dashboard (Next.js) |
cloudflared | cloudflare/cloudflared:latest ¹ | proxy-net | Cloudflare tunnel |
portainer-agent | portainer/agent:latest | host | Portainer remote agent |
promtail | grafana/promtail:latest | — | Log shipping |
The proxy-net Docker bridge allows cloudflared to route to http://level147-net:3000 by container name.
¹ cloudflared uses :latest as an accepted exception — Cloudflare’s tunnel protocol requires a recent client to maintain compatibility with the edge. Pin to a semver tag if stability becomes a concern.
Key configuration files (web VPS)
| File | Path | Purpose |
|---|---|---|
docker-compose.yml | /home/level147.net/docker-compose.yml | Container definitions |
.env | /home/level147.net/.env | Secrets and service URLs |
Tools VPS — services
| Service | Port | Purpose |
|---|---|---|
| Gitea | :3000 (HTTP), :2222 (SSH) | Source control, Docker registry |
| Woodpecker CI | :8000 (UI) | CI/CD pipelines |
| Portainer | :9000 | Container management UI |
All accessible via Tailscale only — not publicly exposed.
Access path (public request)
Browser → Cloudflare CDN (DDoS, WAF, TLS termination) → Cloudflare Access (Zero Trust — identity check) → cloudflared QUIC tunnel → proxy-net Docker bridge → level147-net:3000 (Next.js)CI/CD pipeline
git push → Gitea (tools VPS :3000) → Woodpecker trigger (.woodpecker/build.yml) → docker build + push to gitea.level147.net registry → trigger-deploy step (Woodpecker API, manual event) → .woodpecker/deploy.yml SCP docker-compose.yml → web VPS SSH → docker compose pull + up -d --force-recreateDocs pipeline (separate repo)
The docs site lives in its own repository (Level147/docs.level147.net) with its
own pipeline:
git push → main (docs.level147.net repo) → Woodpecker trigger (.woodpecker.yml) → npm ci + npm run build → npx wrangler pages deploy dist --project-name=level147-docs → Cloudflare Pages (docs.level147.net)CI notes
- Deploy step uses
network_mode: hostat step level (notbackend_options) —backend_options.docker.network_modeis not honoured by the installed Woodpecker agent version. Linter warns but pipeline executes correctly. - Woodpecker secrets (including
CLOUDFLARE_API_TOKEN,WEB_SSH_KEY,GITEA_TOKEN) must be configured via Woodpecker UI under repo or org settings with the Manual event trigger enabled.
Docker socket access
The level147-net container accesses the Docker socket for the Docker Logs page and Portainer fallback:
volumes: - /var/run/docker.sock:/var/run/docker.sockgroup_add: - "988" # docker group GID on web VPSThe socket is mounted without :ro — write access is required for container restart/stop operations performed via the dashboard.
Service reference
| Service | Document |
|---|---|
| Cloudflare Tunnel + Access | Cloudflare |
| Docker socket integration | Docker |
| Tailscale mesh VPN | Tailscale |
| Portainer container management | Portainer |
| Uptime Kuma monitoring | Uptime Kuma |
v0.1.0 · d0d7a20 · 2026-06-26