Skip to content

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 Tailscale

Server roster

HostTailscale IPRole
Web VPS100.101.148.89Ops dashboard, cloudflared tunnel
Tools VPS100.81.122.65Gitea, Woodpecker CI, Portainer
Apps host100.83.125.55Application services (widget data sources)

Web VPS — container stack

ContainerImageNetworkPurpose
level147-netgitea.level147.net/level147/level147.net:prodproxy-netOps dashboard (Next.js)
cloudflaredcloudflare/cloudflared:latest ¹proxy-netCloudflare tunnel
portainer-agentportainer/agent:latesthostPortainer remote agent
promtailgrafana/promtail:latestLog 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)

FilePathPurpose
docker-compose.yml/home/level147.net/docker-compose.ymlContainer definitions
.env/home/level147.net/.envSecrets and service URLs

Tools VPS — services

ServicePortPurpose
Gitea:3000 (HTTP), :2222 (SSH)Source control, Docker registry
Woodpecker CI:8000 (UI)CI/CD pipelines
Portainer:9000Container 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-recreate

Docs 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: host at step level (not backend_options) — backend_options.docker.network_mode is 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
group_add:
- "988" # docker group GID on web VPS

The socket is mounted without :ro — write access is required for container restart/stop operations performed via the dashboard.


Service reference

ServiceDocument
Cloudflare Tunnel + AccessCloudflare
Docker socket integrationDocker
Tailscale mesh VPNTailscale
Portainer container managementPortainer
Uptime Kuma monitoringUptime Kuma

v0.1.0 · d0d7a20 · 2026-06-26