Blog
PLEN

Caddy reverse proxy — patterns które realnie używam

Caddy jest moją domyślną front-line proxy w homelabie. Pokazuję 5 wzorców konfiguracji które mi się sprawdzają — od basic reverse proxy po zaawansowane HSTS/HSTS preload + auth gating.

·3 min read
Caddy reverse proxy — patterns które realnie używam

Nginx, Traefik, HAProxy, każdy ma swoich fanów. Ja używam Caddy. Cztery powody: automatyczne TLS, czytelny config, HTTP/3 out-of-box, brak komplikacji którymi zarządza się 2% userów. Pokażę 5 wzorców z mojego setupu.

Pattern #1: basic reverse proxy z TLS

Najprostsze co kiedykolwiek napisałem:

vikunja.kamilkaletka.dev {
    reverse_proxy localhost:3456
}

To wszystko. Caddy automatycznie:

  • Pobiera cert Let's Encrypt
  • Auto-renewuje co 60 dni
  • Forwardpuje HTTP do HTTPS
  • Włącza HTTP/2 i HTTP/3

Porównaj z nginx-em: 30 linii configu, certbot timer, manual renew handling.

Pattern #2: header rewriting + WebSocket

Niektóre apki potrzebują custom headerów albo WebSocket support:

chat.kamilkaletka.dev {
    reverse_proxy localhost:8080 {
        header_up Host {host}
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}
    }
}

Caddy wykrywa WebSocket sam, nie trzeba proxy_pass http://upstream/upgrade websocket jak w nginx. WS upgrade działa po prostu.

Pattern #3: path-based routing

Jedna domena, wiele backend'ów:

api.kamilkaletka.dev {
    handle_path /v1/* {
        reverse_proxy localhost:3001
    }
    handle_path /v2/* {
        reverse_proxy localhost:3002
    }
    handle_path /admin/* {
        reverse_proxy localhost:3003
    }
    handle {
        respond "Not Found" 404
    }
}

handle_path strip'uje prefix przed forward'em (jak nginx rewrite ... break). handle to else branch.

Pattern #4: forward auth (gating)

Gdy chcę dodać autoryzację przed serwisem który nie ma własnej:

admin.kamilkaletka.dev {
    forward_auth localhost:9000 {
        uri /verify
        copy_headers X-User-Email X-User-Roles
    }
    reverse_proxy localhost:3000
}

Każdy request idzie najpierw do auth serwera (mam Authentik na 9000). Jeśli ten zwróci 200 → request leci do app. Jeśli 401/403 → Caddy zwraca błąd, app nie widzi requestu.

Dodaję headers X-User-Email żeby aplikacja wiedziała kto jest zalogowany.

Pattern #5: HSTS + security headers

Production-ready security headers:

(security_headers) {
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Referrer-Policy "strict-origin-when-cross-origin"
        Permissions-Policy "geolocation=(), microphone=(), camera=()"
        # Remove server headers
        -Server
    }
}
 
vikunja.kamilkaletka.dev {
    import security_headers
    reverse_proxy localhost:3456
}
 
obsidian-sync.kamilkaletka.dev {
    import security_headers
    reverse_proxy localhost:5984
}

Definiuję snippet raz, importuję wszędzie. Każda domena dostaje te same security headers bez powtarzania.

Pułapka: bind mount + reload

Najboleśniejsza pułapka której się nauczyłem.

Caddy w Dockerze, bind mount /etc/caddy/Caddyfile:

volumes:
  - ./Caddyfile:/etc/caddy/Caddyfile

Po edycji Caddyfile robisz:

# NIE DZIAŁA czasami
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
 
# DZIAŁA ZAWSZE
docker restart caddy

Powód: bind mount per-inode. Jak edytujesz plik na hoście (vim, większość edytorów robi temp+rename), Caddy w kontenerze widzi STARY plik bo pamięta ten inode. Restart kontenera = nowy mount = nowy inode.

Alternatywa: edytuj przez docker exec caddy vi, ale to ohydne.

Pattern bonus: rate limiting

Caddy ma plugin caddy-ratelimit:

api.kamilkaletka.dev {
    rate_limit {
        zone api_zone {
            key {remote_host}
            events 60
            window 1m
        }
    }
    reverse_proxy localhost:3001
}

60 requestów na minutę z jednego IP. Powyżej → 429. Plugin wymaga build customowego Caddy (z xcaddy), ale ~5 minut roboty.

Czego Caddy NIE robi dobrze

1. Bardzo skomplikowane regex routing. Nginx ma więcej narzędzi.

2. Module-based plugin system jest słabszy. Plugins idą do binarki, nie load'ują dynamicznie. Każdy plugin = recompile.

3. Performance pod ekstremalnym load'em. Dla > 10k req/s nginx wciąż wygrywa. Dla normalnego homelabu (max 100 req/s) nie ma znaczenia.


Caddy zamienia 30-liniowe nginx'y na 3-liniowe entries. Auto TLS to feature który dla mnie sam zwracał całe migration przez tydzień. Jeśli jeszcze siedzisz na nginx + certbot, daj sobie weekend i przerzuć się.