Caddy Behind Traefik: Reverse Proxy Architecture & Zero-Trust Ingress Routing
Deploy Caddy behind Traefik for hardened TLS termination and dynamic reverse proxying. Full guide from bare-metal prep to zero-trust access and automated backups.
Caddy Behind Traefik: Production-Grade Homelab Reverse Proxy Architecture & Zero-Trust Deployment 2026
1. Executive Summary & Architecture Overview
This guide details the deployment of a layered reverse-proxy architecture integrating Traefik as the dynamic edge entry point and Caddy as the application-layer handler for specific services requiring advanced TLS or WebSocket management. Self-hosting this configuration grants complete ownership of certificate chains, access logs, and routing logic—eliminating reliance on commercial SaaS load balancers that retain metadata or impose egress fees.
Architecture Flow:
Internet traffic enters the homelab via port 443, handled by Traefik. Traefik manages global SSL termination using Let's Encrypt and routes requests based on Hostnames (Host()) and Path Prefixes. For services requiring granular control (e.g., complex WebSocket upgrades or specific HTTP/2 push), Traefik forwards traffic to Caddy containers operating on internal Docker networks. Caddy can terminate TLS again for end-to-end encryption or pass through plain HTTP within the private subnet. This separation ensures high availability at the edge (Traefik) and flexibility at the service layer (Caddy).
2. Hardware, OS & Network Requirements
| Component | Minimum Specification | Recommended Specification |
|---|---|---|
| CPU | 2 Cores | 4+ Cores (NVMe preferred for I/O) |
| RAM | 2 GB | 4 GB+ |
| Storage | 32 GB SSD | 100 GB NVMe (for log rotation) |
| Network | 100 Mbps | 1 Gbps+ with IPv6 support |
| OS | Debian 12 / Ubuntu 22.04 LTS | Debian 12 (Bookworm) Minimal |
Port Configuration:
- Inbound: TCP 80 (HTTP-01 Challenge), TCP 443 (HTTPS).
- Internal: TCP 8080 (Traefik Dashboard - restricted), TCP 80/443 (Caddy internal).
3. Step 1: Host Preparation & Directory Layout
Establish a strict directory hierarchy to isolate configuration, data, and backups.
# Create base directories
sudo mkdir -p /opt/homelab/{traefik,caddy}/{config,data}
sudo mkdir -p /opt/homelab/caddy-behind-traefik/{scripts,backups}
# Set ownership to non-root user (example: pi/serveradmin)
# Replace UID/GID with your system user IDs
sudo chown -R 1000:1000 /opt/homelab
# Verify permissions
ls -la /opt/homelab/
4. Step 2: Production-Grade docker compose.yaml
We utilize the modern Compose Spec (no version key). This stack deploys Traefik for edge routing and Caddy for a sample service backend.
File: /opt/homelab/caddy-behind-traefik/docker compose.yaml
services:
traefik:
image: traefik:v3.2.2
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- type: bind
source: /opt/homelab/traefik/config
target: /etc/traefik
- type: bind
source: /opt/homelab/traefik/data
target: /data
environment:
- TZ=${TZ}
- CF_API_EMAIL=${CF_API_EMAIL}
- CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`traefik.local.example.com`)"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=auth@docker"
- "traefik.http.middlewares.auth.basicauth.users=${TRAEFIK_ADMIN_USER}:${TRAEFIK_ADMIN_PASS}" # Generate with htpasswd
networks:
- public
- internal
caddy-service:
image: caddy:2.9.1
container_name: caddy-service
restart: unless-stopped
security_opt:
- no-new-privileges:true
labels:
- "traefik.enable=true"
- "traefik.http.routers.caddy.service=caddy-service"
- "traefik.http.services.caddy-service.loadbalancer.server.port=80"
# Example WebSocket handling
- "traefik.http.routers.caddy.rule=Host(`app.local.example.com`)"
environment:
- PUID=${PUID}
- PGID=${PGID}
- TZ=${TZ}
volumes:
- type: bind
source: /opt/homelab/caddy/data
target: /data
- type: bind
source: /opt/homelab/caddy/config
target: /config
networks:
- internal
networks:
public:
driver: bridge
internal:
driver: bridge
internal: true
Environment Variables (.env):
Create /opt/homelab/caddy-behind-traefik/.env:
TZ=UTC
PUID=1000
PGID=1000
CF_API_EMAIL=user@example.com
CF_DNS_API_TOKEN=your_cloudflare_token_here
TRAEFIK_ADMIN_USER=admin
# Generate password hash: docker run --rm traefik htpasswd -b user password
TRAEFIK_ADMIN_PASS=hash_here
5. Step 3: Deployment & Health Verification
Deploy the stack using the modern docker compose command:
cd /opt/homelab/caddy-behind-traefik
docker compose up -d
Verify status and health:
docker compose ps
docker compose logs -f traefik
Check Traefik API directly inside the container:
docker exec traefik traefik api --raw --output json | jq '.routers'
6. Step 4: Reverse Proxy, Domain & SSL Hardening
Traefik manages the external SSL certificate via DNS-01 challenges (Cloudflare). Internal communication between Traefik and Caddy remains unencrypted within the isolated internal network by default, but can be secured via mutual TLS (mTLS) if required.
Essential Proxy Headers: Traefik injects standard headers automatically. Ensure Caddy is configured to trust these:
X-Forwarded-For: Real client IP.X-Forwarded-Proto: Scheme (http/https).X-Forwarded-Host: Original hostname.
HSTS Configuration:
Add to Traefik static config (/opt/homelab/traefik/config/traefik.yml):
http:
middlewares:
hsts:
headers:
stsPreload: true
stsSeconds: 31536000
stsIncludeSubdomains: true
Apply middleware to routers via labels: traefik.http.routers.<name>.middlewares=hsts@file.
7. Step 5: Zero-Trust Remote Access with Tailscale
Expose the dashboard and services securely without opening ports on your ISP modem.
- Install Tailscale on the host:
curl -fsSL https://tailscale.com/install.sh | sh sudo tailscale up - Add Tailscale as a sidecar or integrated service in the compose file to share the tailnet interface, or simply route traffic through the host's Tailscale IP (100.x.x.x).
- For containerized access, add a Tailscale daemonset or use
--network=hostwith caution. The modern approach uses the Tailscale Kubernetes operator or a dedicated container:tailscale: image: tailscale/tailscale:latest container_name: tailscale restart: unless-stopped volumes: - ./tailscale-state:/var/lib/tailscale - /dev/net/tun:/dev/net/tun environment: - TS_AUTHKEY=tailscale-key-here - TS_USERSPACE=false network_mode: host - Access
https://app.local.example.comvia the Tailscale MagicDNS name or 100.x.x.x address.
8. Step 6: Automated Backup & Disaster Recovery
Create /opt/homelab/caddy-behind-traefik/scripts/backup.sh:
#!/bin/bash
set -e
BACKUP_DIR="/opt/homelab/backups"
DATE=$(date +%Y%m%d-%H%M%S)
CONTAINER_DIR="/opt/homelab/caddy-behind-traefik"
mkdir -p ${BACKUP_DIR}/${DATE}
echo "Stopping services..."
docker compose -f ${CONTAINER_DIR}/docker-compose.yaml down
echo "Dumping Traefik Data..."
tar -czf ${BACKUP_DIR}/${DATE}/traefik-data.tar.gz /opt/homelab/traefik/data
echo "Dumping Caddy Config..."
tar -czf ${BACKUP_DIR}/${DATE}/caddy-config.tar.gz /opt/homelab/caddy/config
echo "Starting services..."
docker compose -f ${CONTAINER_DIR}/docker-compose.yaml up -d
echo "Backup complete: ${BACKUP_DIR}/${DATE}"
Make executable and schedule via cron:
chmod +x /opt/homelab/caddy-behind-traefik/scripts/backup.sh
crontab -e
# Add line:
0 2 * * * /opt/homelab/caddy-behind-traefik/scripts/backup.sh >> /var/log/homelab-backup.log 2>&1
9. Step 7: Deep Troubleshooting Matrix
| Error / Symptom | Root Cause | Verified Resolution |
|---|---|---|
EACCES: permission denied on volume mount |
Host folder owned by root, container runs as non-root | sudo chown -R 1000:1000 /opt/homelab/* |
502 Bad Gateway on specific routes |
Traefik cannot resolve upstream or Caddy not running | Check docker compose ps; ensure Caddy is healthy on internal network |
| Let's Encrypt cert fails to issue | DNS-01 challenge timeout | Verify CF_DNS_API_TOKEN has Zone.DNS write permissions; check Traefik logs for acme errors |
| WebSocket connection drops | Missing Upgrade header passthrough |
Ensure Traefik middleware preserves Connection and Upgrade headers |
Container crashes on startup (oom_kill) |
Insufficient memory limit | Increase host RAM or set mem_limit in compose file |
10. Step 8: Frequently Asked Questions (FAQ)
Q: Should I use Watchtower for automatic updates? A: No. For production homelabs, automatic updates can introduce breaking changes to configuration formats. Manually pinning versions in the Compose file and updating quarterly after testing is the recommended stable workflow.
Q: Can I use Nginx Proxy Manager instead of Traefik? A: Yes, but you lose the dynamic service discovery capabilities native to Docker. Nginx Proxy Manager requires manual entry or external webhook integration for new services, whereas Traefik reads labels directly.
Q: How do I migrate from an older version: '3' compose file?
A: Remove the top-level version key. Migrate ports mappings to the new syntax if using secrets/configs. Run docker compose convert to validate the new spec before deploying.
Q: Is it safe to expose the Traefik Dashboard publicly? A: Never expose the dashboard to the public internet without strict Basic Auth or IP whitelisting. The provided configuration restricts it to a internal hostname with HTTP Basic Authentication.
Q: How do I debug Caddy internal errors?
A: Access logs via docker compose logs caddy-service. For configuration validation, use docker exec caddy-service caddy validate --config /config/Caddyfile.
Was this homelab guide valuable to you?
Your reaction helps our research team prioritize hardware test benches.
Tariq Al-Mansoor
Principal Systems Architect Peer-ReviewedInfrastructure engineer focused on sovereign homelab deployments, reproducible bare-metal setups, and high-availability Linux clusters.
Related Guides
On this page
Tap any section to jump directly
Community Technical Desk & Troubleshooting
0 Homelab Technical DeskEncountering an error, permission issue, or port conflict with this stack? Submit your setup question below — our engineering team reviews and replies with tested solutions.
No technical questions yet for this guide.
Have a question or running into an error? Ask above and our technical support team will reply in ~2 minutes!