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

Web VPS — container stack

ContainerImageNetworkPurpose
level147-netgitea.level147.net/level147/level147.net:prodproxy-netOps dashboard (Next.js)
cloudflaredcloudflare/cloudflared:latestproxy-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.

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), :9000 (agent gRPC)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)

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: 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:ro
group_add:
- "988" # docker group GID on web VPS

The 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

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