Files
homelab/setup/caddy.md
T
lerko96 cd454b2926 docs(public): populate phase 2 content
Full public/ directory — services, network, decisions, security,
inventory, rebuild sequence, and per-LXC setup guides. Sourced from
wiki. No secrets or WAN IPs included.
2026-04-17 21:23:59 -04:00

4.8 KiB

Caddy (infra LXC) Setup

Overview

The infra LXC (10.2.0.20) in VLAN 1020 runs Caddy as the internal reverse proxy. It handles TLS termination for all *.lerkolabs.com services using wildcard certs via Cloudflare DNS-01 challenge. Also hosts ntfy (push notifications) and Uptime Kuma (service monitoring) as co-located services.

Service Port Domain
Caddy 80/443 reverse proxy — no direct domain
ntfy 8090 ntfy.lerkolabs.com
Uptime Kuma 3001 uptime.lerkolabs.com

Prerequisites

  • LXC created at 10.2.0.20 in VLAN 1020
  • Cloudflare API token with Zone → DNS → Edit permissions for lerkolabs.com (stored in Vaultwarden)
  • Pi-hole DNS record: *.lerkolabs.com → 10.2.0.20

LXC Spec

Property Value
Hostname infra
IP 10.2.0.20/24
Gateway 10.2.0.1
Cores 2
RAM 1GB
Template debian-12-standard
Nesting ✓ (required for Docker)

Installation

apt update && apt upgrade -y
apt install -y curl nano ufw
timedatectl set-timezone America/Chicago
curl -fsSL https://get.docker.com | sh
systemctl enable docker

Directory Structure

/opt/docker/
├── caddy/
│   ├── Caddyfile
│   ├── docker-compose.yml
│   ├── Dockerfile
│   ├── .env
│   ├── data/
│   ├── config/
│   └── logs/
└── infra/
    ├── uptimekuma/
    │   ├── docker-compose.yml
    │   └── data/
    └── ntfy/
        ├── docker-compose.yml
        ├── server.yml
        └── data/

Caddy Deployment

Dockerfile (custom build with Cloudflare DNS plugin)

FROM caddy:2-builder AS builder
RUN xcaddy build \
    --with github.com/caddy-dns/cloudflare

FROM caddy:2-alpine
COPY --from=builder /usr/bin/caddy /usr/bin/caddy

.env

CLOUDFLARE_API_TOKEN=<token from Vaultwarden: homelab/cloudflare-api>
chmod 600 /opt/docker/caddy/.env

docker-compose.yml

services:
  caddy:
    build: .
    container_name: caddy
    restart: unless-stopped
    network_mode: host
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./data:/data
      - ./config:/config
      - ./logs:/logs
    env_file:
      - .env

Caddyfile structure

{
    email your@email.com
    acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}

# Forward auth snippet — reuse in every protected service block
(authentik_forward_auth) {
    forward_auth 10.2.0.25:9000 {
        uri /outpost.goauthentik.io/auth/caddy
        copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email \
                     X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks \
                     X-Authentik-Meta-Outpost X-Authentik-Meta-Provider \
                     X-Authentik-Meta-App X-Authentik-Meta-Version
        trusted_proxies private_ranges
    }
}

# Authentik
auth.lerkolabs.com {
    reverse_proxy 10.2.0.25:9000
}

# Pi-hole (forward auth)
pihole.lerkolabs.com {
    import authentik_forward_auth
    reverse_proxy 10.2.0.11:80
}

# Add remaining services following the same pattern
# Services with native OIDC (Outline, Gitea, Vikunja): no forward auth needed
# Services without OIDC: import authentik_forward_auth

Build and start

cd /opt/docker/caddy
docker compose build
docker compose up -d
docker logs -f caddy
# Wait for: "certificate obtained successfully"

ntfy Deployment

server.yml

base-url: https://ntfy.lerkolabs.com
listen-http: ":8090"
cache-file: /var/cache/ntfy/cache.db
auth-file: /var/lib/ntfy/auth.db
auth-default-access: deny-all
behind-proxy: true

docker-compose.yml

services:
  ntfy:
    image: binwiederhier/ntfy:latest
    container_name: ntfy
    restart: unless-stopped
    command: serve
    ports:
      - "8090:8090"
    volumes:
      - ./server.yml:/etc/ntfy/server.yml:ro
      - ./data:/var/cache/ntfy
      - ./data:/var/lib/ntfy
cd /opt/docker/infra/ntfy && docker compose up -d
docker exec -it ntfy ntfy user add --role=admin <username>

Uptime Kuma Deployment

services:
  uptime-kuma:
    image: louislam/uptime-kuma:latest
    container_name: uptime-kuma
    restart: unless-stopped
    ports:
      - "3001:3001"
    volumes:
      - ./data:/app/data
cd /opt/docker/infra/uptimekuma && docker compose up -d

Caddy Reload

After editing the Caddyfile:

docker exec caddy caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
docker exec caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile

Verification

# Cert issued
docker logs caddy | grep "certificate obtained"

# Service reachable
curl -I https://pihole.lerkolabs.com
# Expected: HTTP/2 200

# Ports bound
ss -tlnp | grep -E "443|8090|3001"