caddy LXC¶
Overview¶
| Property | Value |
|---|---|
| Hostname | caddy |
| IP Address | 192.168.0.208 |
| VMID | 110 |
| OS | Alpine Linux 3.23 |
| CPU | 1 core |
| RAM | 256 MB |
| Disk | 3 GB (local-lvm) |
| Purpose | Reverse proxy for all .lan services (HTTP + HTTPS) |
Running Services¶
| Service | Description |
|---|---|
caddy |
Caddy web server / reverse proxy |
Open Ports¶
| Port | Protocol | Service |
|---|---|---|
| 22 | TCP | SSH |
| 80 | TCP | HTTP - same handlers as HTTPS, for devices without the CA cert |
| 443 | TCP | HTTPS reverse proxy with TLS |
Caddy¶
Version: v2.11.2 (installed via community-scripts Alpine LXC)
Config: /etc/caddy/Caddyfile
Certs: /etc/caddy/certs/
TLS Certificates¶
Generated with mkcert using a local CA (CAROOT=/etc/caddy/certs):
| File | Description |
|---|---|
rootCA.pem |
Local CA root - must be imported into every browser/device |
rootCA-key.pem |
Local CA private key |
lan.pem |
Server cert (all .lan domains as explicit SANs) |
lan-key.pem |
Server cert private key (owner: root:caddy, mode: 640) |
Cert expiry: 2028-06-30
Note: Wildcard *.lan certs are rejected by Firefox (second-level wildcard). All .lan domains must be listed explicitly as SANs in the cert. To add a new domain, regenerate the cert with mkcert including the new domain name.
Regenerating the cert¶
pct exec 110 -- sh -c 'CAROOT=/etc/caddy/certs /usr/local/bin/mkcert \
-cert-file /etc/caddy/certs/lan.pem \
-key-file /etc/caddy/certs/lan-key.pem \
proxmox.lan adguard.lan komodo.lan karakeep.lan n8n.lan ollama.lan \
jellyfin.lan homepage.lan immich.lan bentopdf.lan docuseal.lan \
qbit.lan sonarr.lan form.lan syncthing.lan \
suggestarr.lan notifiarr.lan calibre.lan seerr.lan radarr.lan \
scrutiny.lan prowlarr.lan freshrss.lan \
netdata.lan haos.lan vaultwarden.lan syncthing-nex.lan homelable.lan'
pct exec 110 -- chown root:caddy /etc/caddy/certs/lan-key.pem
pct exec 110 -- chmod 640 /etc/caddy/certs/lan-key.pem
pct exec 110 -- rc-service caddy restart
Caddyfile structure¶
All service handlers are defined once in a (lan_services) snippet and imported by both the HTTPS and HTTP blocks. This avoids duplication:
(lan_services) {
@service host service.lan
handle @service { reverse_proxy ... }
...
handle { respond 404 }
}
*.lan {
tls /etc/caddy/certs/lan.pem /etc/caddy/certs/lan-key.pem
import lan_services
}
http://*.lan {
import lan_services
}
Adding a new service¶
-
AdGuard - add DNS rewrite:
newservice.lan→192.168.0.208 -
Caddyfile - add a new handler inside the
(lan_services)snippet, before the finalhandle { respond 404 }: -
Cert - regenerate with the new domain added to the list (see "Regenerating the cert" above)
-
DNS cache - flush on the client:
Installing the root CA on a new device¶
# Pull rootCA.pem from the LXC via Proxmox
ssh root@192.168.0.109 "pct pull 110 /etc/caddy/certs/rootCA.pem /tmp/rootCA.pem"
scp root@192.168.0.109:/tmp/rootCA.pem ~/mkcert-rootCA.pem
Firefox (desktop): Settings → Privacy & Security → View Certificates → Authorities → Import → select mkcert-rootCA.pem → check "Trust this CA to identify websites"
Android (system cert): Copy the .pem file to the device → Settings → Security → Install certificate → CA certificate. This enables HTTPS in Chrome and regular Firefox, but not Firefox Nightly (which ignores user certs).
Firefox Nightly on Android: Nightly does not trust Android user-installed CA certs. Use http://service.lan (port 80) instead - the HTTP listener serves identical content without TLS. The Tailscale mesh provides transport encryption so plain HTTP is acceptable over Tailscale.
Proxied Services¶
All .lan domains resolve to 192.168.0.208 (Caddy) via AdGuard DNS rewrites.
| Domain | Backend |
|---|---|
| proxmox.lan | https://192.168.0.109:8006 (tls_insecure_skip_verify) |
| adguard.lan | http://192.168.0.111:80 |
| komodo.lan | http://192.168.0.105:9120 |
| karakeep.lan | http://192.168.0.128:3000 |
| n8n.lan | http://192.168.0.112:5678 |
| ollama.lan | http://192.168.0.231:11434 |
| jellyfin.lan | http://192.168.0.110:8096 |
| homepage.lan | http://192.168.0.110:3002 |
| immich.lan | http://192.168.0.110:2283 |
| bentopdf.lan | http://192.168.0.110:3000 |
| docuseal.lan | http://192.168.0.110:3003 |
| qbit.lan | http://192.168.0.110:8080 (X-Forwarded-Proto: https) |
| sonarr.lan | http://192.168.0.110:8989 |
| form.lan | http://192.168.0.110:3004 |
| syncthing.lan | http://192.168.0.110:8384 |
| suggestarr.lan | http://192.168.0.110:5000 |
| notifiarr.lan | http://192.168.0.110:5454 |
| calibre.lan | http://192.168.0.110:8085 |
| seerr.lan | http://192.168.0.110:5055 |
| radarr.lan | http://192.168.0.110:7878 |
| scrutiny.lan | http://192.168.0.110:8082 |
| prowlarr.lan | http://192.168.0.110:9696 |
| freshrss.lan | http://192.168.0.110:8083 |
| netdata.lan | http://192.168.0.109:19999 |
| haos.lan | http://192.168.0.202:8123 |
| vaultwarden.lan | https://192.168.0.219:8000 (tls_insecure_skip_verify) |
| syncthing-nex.lan | http://192.168.0.100:8384 |
| homelable.lan | http://192.168.0.110:3001 |
Note on qbit.lan: qBittorrent 5.1+ reads X-Forwarded-Proto: https from trusted proxies to automatically set the Secure flag on its session cookie. The Caddy block explicitly sets this header even on HTTP requests so the behavior is consistent.
Lessons Learned¶
- Wildcard
*.lanrejected by Firefox: Firefox does not accept second-level wildcard certs (e.g.*.lan). All domains must be listed explicitly as SANs. mkcert warns about this during generation. - Key file permissions: Caddy runs as the
caddyuser. The private key must bechown root:caddyandchmod 640, otherwise Caddy fails to start with "permission denied". - DNS cache on Linux clients: After adding new AdGuard rewrites, Linux clients may need
resolvectl flush-cachesbefore the new domain resolves. - mkcert not in Alpine apk: mkcert is not available in Alpine's package repos. Install the binary directly from GitHub releases.
- Installation: Deployed via community-scripts Alpine LXC script (successor to tteck/Proxmox scripts).
- HTTP + HTTPS in one Caddyfile: Caddy does not allow a
tlsdirective in a block that matches both HTTP and HTTPS. Solution: define handlers once in a named snippet(lan_services)andimportit from both the*.lan(HTTPS) andhttp://*.lan(HTTP) blocks. - Firefox Nightly Android cannot trust user CAs: Android user-installed CA certificates are ignored by Firefox Nightly regardless of
security.enterprise_roots.enabled. The workaround is HTTP access on port 80.