Skip to content

Network Architecture

How traffic flows from the internet to internal services.


Overview

┌─────────────────────────────────────────────────────────────┐
│                     Cloudflare Edge                         │
│         (SSL termination, DDoS, WAF, caching)               │
│                                                             │
│  atlantis.minnova.io ──┐                                    │
│  zitadel.minnova.io ───┼── CNAME to tunnel                  │
│  grafana.minnova.io ───┘                                    │
└─────────────────────────────┬───────────────────────────────┘
                              │ Tunnel (outbound connection)
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                 Hetzner Private Network                     │
│                                                             │
│  ┌─────────────────┐                                        │
│  │  Ingress Node   │  Cloudflared + Reverse Proxy           │
│  │  (+ Tailscale)  │  (Caddy now, Traefik with Nomad)       │
│  └────────┬────────┘                                        │
│           │ Private network (10.0.x.x)                      │
│           │                                                 │
│     ┌─────┴─────┬─────────────┐                             │
│     ▼           ▼             ▼                             │
│  ┌───────┐  ┌───────┐  ┌───────────┐                        │
│  │Worker1│  │Worker2│  │  Worker3  │  No public IP          │
│  │       │  │       │  │           │  No cloudflared        │
│  └───────┘  └───────┘  └───────────┘  No Tailscale          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Key Decisions

Decision Choice Why
Edge/CDN Cloudflare Free tier, DDoS, WAF, tunnel support
Ingress Cloudflare Tunnel No public IPs needed, outbound-only
Reverse proxy Caddy (now), Traefik (Nomad) Auto-discovery, simple config
SSH access Tailscale on ingress node Zero trust, no exposed SSH
Private network Hetzner vSwitch Free, isolates workers

Traffic Flow

HTTP/HTTPS (Application Traffic)

User → Cloudflare Edge → Tunnel → Caddy/Traefik → Service
  1. User visits atlantis.minnova.io
  2. DNS resolves to Cloudflare (CNAME to tunnel)
  3. Cloudflare terminates SSL, applies WAF rules
  4. Traffic flows through tunnel to your infrastructure
  5. Reverse proxy routes by hostname to correct service

SSH (Admin Access)

Admin → Tailscale → Ingress Node → SSH jump → Workers
  1. Admin connects via Tailscale
  2. SSH to ingress node (has Tailscale)
  3. SSH jump to private workers (no public IP)

Components

Cloudflare Tunnel

Outbound-only connection from your infrastructure to Cloudflare edge. Eliminates need for: - Public IPs on services - Firewall port management - SSL certificate management

One tunnel, multiple hostnames:

tunnel: <tunnel-id>
ingress:
  - hostname: "*.minnova.io"
    service: http://localhost:80  # Reverse proxy
  - service: http_status:404

All DNS records CNAME to same tunnel. Routing happens inside your infrastructure.

Reverse Proxy

Routes traffic by hostname to internal services.

Current (Caddy with Docker/Podman):

# Labels on container auto-configure routing
services:
  atlantis:
    labels:
      caddy: atlantis.minnova.io
      caddy.reverse_proxy: "{{upstreams 4141}}"

Future (Traefik with Nomad):

# Nomad service block auto-registers with Consul
service {
  name = "atlantis"
  port = "http"
  tags = [
    "traefik.enable=true",
    "traefik.http.routers.atlantis.rule=Host(`atlantis.minnova.io`)",
  ]
}

Tailscale (Ingress Node Only)

Provides secure admin access without exposing SSH publicly.

  • Installed only on ingress node
  • Other nodes accessible via SSH jump
  • Integrates with Zitadel OIDC for authentication

Hetzner Private Network

resource "hcloud_network" "internal" {
  name     = "internal"
  ip_range = "10.0.0.0/16"
}

resource "hcloud_network_subnet" "services" {
  network_id   = hcloud_network.internal.id
  type         = "cloud"
  network_zone = "eu-central"
  ip_range     = "10.0.1.0/24"
}

Ingress node: Public IP + private IP + Tailscale Workers: Private IP only


Scaling Path

Phase Architecture
1 VPS Everything on one machine (current)
2-3 VPS Ingress node + workers on private network
4+ VPS Dedicated ingress, Nomad cluster on workers

Single Node (Current)

┌─────────────────────────────┐
│        Single VPS           │
│                             │
│  - Cloudflared              │
│  - Caddy                    │
│  - Tailscale                │
│  - Atlantis                 │
│  - (future: Zitadel, etc.)  │
└─────────────────────────────┘

Multi-Node (Future)

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│   Ingress    │     │   Worker 1   │     │   Worker 2   │
│              │     │              │     │              │
│ Cloudflared  │     │ Nomad client │     │ Nomad client │
│ Traefik      │────▶│ Atlantis     │     │ Zitadel      │
│ Tailscale    │     │ Prometheus   │     │ Grafana      │
│ Nomad server │     │              │     │              │
└──────────────┘     └──────────────┘     └──────────────┘
      │                    │                    │
      └────────────────────┴────────────────────┘
                   Private Network

Nomad/Kubernetes Compatibility

This architecture translates directly:

Component Ansible+Podman Nomad Kubernetes
Cloudflared Systemd service Job Deployment
Reverse proxy Caddy container Traefik job Ingress controller
Services Systemd services Jobs Deployments
Discovery Manual config Consul K8s Services

The pattern stays the same, only orchestration changes.