Skip to content

Architecture

The setup is split into an edge/infra node (yggdrasil), a primary application node (midgard), and a cloud ARM application node (alfheim). External traffic enters only through Cloudflare Tunnel → Caddy instead of directly exposed ports, and the Tailscale tailnet is the internal network boundary between hosts.

flowchart TD
    internet["Internet users"]
    cloudflare["Cloudflare<br/>DNS / Tunnel edge"]

    subgraph yggdrasil["yggdrasil: edge / infra node"]
        cloudflared["cloudflared<br/>Cloudflare Tunnel client"]
        caddy["Caddy<br/>HTTPS ingress / reverse proxy"]
        blog["Dev with Min blog<br/>Caddy file_server"]
        docsSite["Docs site<br/>Caddy file_server"]
        kuma["Uptime Kuma<br/>127.0.0.1:3001"]
        beszelHub["Beszel hub<br/>:8090"]
        vlogs["VictoriaLogs<br/>:9428"]
        yShipper["beszel-agent / vlagent"]
    end

    subgraph tailnet["Tailscale tailnet"]
        midgardDns["midgard.tail6fc192.ts.net"]
        alfheimDns["alfheim.tail6fc192.ts.net"]

        subgraph midgard["midgard: application host"]
            homepage["Homepage dashboard<br/>:8082"]
            forgejo["Forgejo<br/>:3000"]
            vaultwarden["Vaultwarden<br/>:8222"]
            mShipper["beszel-agent / vlagent"]
        end

        subgraph alfheim["alfheim: OCI ARM application host"]
            jamyePlz["jamye-plz<br/>:8080"]
            aShipper["beszel-agent / vlagent"]
        end
    end

    internet --> cloudflare
    cloudflare --> cloudflared
    cloudflared -->|"home/blog/git/vault/jamye-plz/status/docs.ridewithmin.com<br/>https://localhost:443"| caddy

    caddy -->|"status.ridewithmin.com"| kuma
    caddy -->|"home.ridewithmin.com"| homepage
    caddy -->|"blog.ridewithmin.com"| blog
    caddy -->|"git.ridewithmin.com"| forgejo
    caddy -->|"vault.ridewithmin.com"| vaultwarden
    caddy -->|"jamye-plz.ridewithmin.com"| jamyePlz
    caddy -->|"beszel.ridewithmin.com<br/>tailnet only"| beszelHub
    caddy -->|"logs.ridewithmin.com<br/>tailnet only"| vlogs
    caddy -->|"docs.ridewithmin.com"| docsSite

    caddy -.->|backend access over Tailscale| midgardDns
    caddy -.->|backend access over Tailscale| alfheimDns
    midgardDns -.-> homepage
    midgardDns -.-> forgejo
    midgardDns -.-> vaultwarden
    alfheimDns -.-> jamyePlz

    yShipper --> beszelHub
    yShipper --> vlogs
    mShipper -.->|metrics WebSocket + journald logs<br/>over Tailscale| beszelHub
    mShipper -.-> vlogs
    aShipper -.->|metrics WebSocket + journald logs<br/>over Tailscale| beszelHub
    aShipper -.-> vlogs

Who can reach what across these boundaries (public Internet, tailnet, localhost) is covered in the security model.

Shared system configuration

All hosts load the same common modules through flake.nix.

Module Purpose
modules/base.nix flakes/nix-command, systemd-boot, NetworkManager, firewall
modules/gc.nix weekly Nix GC + automatic store optimisation
modules/swap.nix zram swap (no separate swap partition)
modules/users.nix operator poby (wheel, passwordless sudo)
modules/ssh.nix OpenSSH, password/root login disabled
modules/tailscale.nix Tailscale
modules/secrets.nix sops-nix base configuration
services/log-shipper.nix journald → VictoriaLogs shipping on every host (journal-upload + vlagent)
services/beszel/agent.nix Beszel metrics agent on every host

Storage

Disk layout is declared with disko. All hosts use a simple single-disk GPT layout.

GPT partition table
512M EFI System Partition  -> /boot, vfat
remaining disk             -> /, ext4

User environment

Home Manager is enabled through the NixOS module and applied as part of each host switch. It is used only for the poby operator environment, not for long-running services. The shared profiles (home/poby/base.nix, ops.nix) carry shell/Git/tmux configuration and operator tools such as age, sops, and just; per-host profiles add host-specific aliases.