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 |
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.
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), :9000 (agent gRPC) | 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)
git push (docs/** changed) → Woodpecker trigger (.woodpecker/docs.yml) → npm ci + npm run build (inside docs/) → 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.sock:rogroup_add: - "988" # docker group GID on web VPSThe socket is mounted read-only. Write operations (start/stop containers) use the Docker API via the socket — the :ro flag permits this; it only prevents mount of additional filesystems.
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 |