Skip to content

Caddy Reverse Proxy Configuration

Caddy configuration for cronova.dev and verava.ai across all environments. Created 2026-01-14.

WARNING (2026-03-10): This doc is a pre-deployment design from January 2026 and does NOT reflect the current Caddyfile. Key differences:

  • Docker VM Caddy is a custom build with caddy-dns/cloudflare for DNS-01 TLS (not Let's Encrypt ACME)
  • Authelia forward auth protects 6 services (not shown below)
  • BentoPDF replaced Stirling-PDF (Kuatia)
  • Many subdomains below (api.cronova.dev, saas.cronova.dev, app.verava.ai) are speculative and don't exist
  • Actual Caddyfile: docker/fixed/docker-vm/networking/caddy/Caddyfile
  • See services.md for the current access matrix

Overview

┌─────────────────────────────────────────────────────────────────────────┐
│                              INTERNET                                    │
└───────────────────────────────────┬─────────────────────────────────────┘
                             [Cloudflare]
                              DNS + CDN
                    ┌───────────────┼───────────────┐
                    │               │               │
             [Cloudflare      [VPS Caddy]    [Tailscale]
               Pages]         Public Proxy    Internal Only
                    │               │               │
              ┌─────┴─────┐   ┌─────┴─────┐   ┌─────┴─────┐
              │           │   │           │   │           │
         www.cronova  docs.  vault.    www.   home.    media.
         cronova.dev  cronova status.  verava cronova  cronova
                             notify.   app.   btc.     nas.
                             api.      api.   git.
                             saas.
└─────────────────────────────────────────────────────────────────────────┘

Traffic Routing

Subdomain Destination Access

| <www.cronova.dev> | Cloudflare Pages | Public | | docs.cronova.dev | Cloudflare Pages | Public | | vault.cronova.dev | VPS → Fixed Homelab (Tailscale) | Public | | status.cronova.dev | VPS localhost:3001 | Public | | notify.cronova.dev | VPS localhost:80 | Public | | api.cronova.dev | VPS localhost:8080 | Public | | saas.cronova.dev | VPS localhost:3000 | Public | | <www.verava.ai> | VPS static files | Public | | app.verava.ai | VPS localhost:4000 | Public | | api.verava.ai | VPS localhost:4001 | Public | | jara.cronova.dev | Fixed Homelab (Tailscale only) | Private | | yrasema.cronova.dev | Fixed Homelab (Tailscale only) | Private | | btc.cronova.dev | RPi 4 Start9 (Tailscale only) | Private | | nas.cronova.dev | NAS (Tailscale only) | Private | | git.cronova.dev | MacBook (Tailscale only) | Private |


VPS Caddyfile

Primary reverse proxy for all public services.

# =============================================================================
# VPS Caddyfile - cronova.dev + verava.ai
# Location: /etc/caddy/Caddyfile
# =============================================================================

# Global options
{
    email augusto@cronova.dev

    # Staging for testing (uncomment to avoid rate limits)
    # acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}

# =============================================================================
# CRONOVA.DEV - Developer/Homelab Services
# =============================================================================

# Root redirect
cronova.dev {
    redir https://www.cronova.dev{uri} permanent
}

# Vault - Password manager (proxied to Fixed Homelab via Tailscale)
vault.cronova.dev {
    reverse_proxy 100.68.63.168:8843 {
        # Health check
        health_uri /alive
        health_interval 30s

        # Timeouts
        transport http {
            dial_timeout 10s
            response_header_timeout 30s
        }
    }

    # Security headers
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
        Referrer-Policy "strict-origin-when-cross-origin"
    }
}

# Status - Uptime monitoring
status.cronova.dev {
    reverse_proxy localhost:3001

    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options "nosniff"
    }
}

# Notify - Push notifications
notify.cronova.dev {
    reverse_proxy localhost:80

    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options "nosniff"
    }
}

# API - Public developer APIs
api.cronova.dev {
    reverse_proxy localhost:8080

    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options "nosniff"
        # CORS for API
        Access-Control-Allow-Origin "*"
        Access-Control-Allow-Methods "GET, POST, OPTIONS"
        Access-Control-Allow-Headers "Content-Type, Authorization"
    }
}

# SaaS - Micro SaaS applications
saas.cronova.dev {
    reverse_proxy localhost:3000

    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
    }
}

# =============================================================================
# VERAVA.AI - Business/Supply Chain Services
# =============================================================================

# Root redirect
verava.ai {
    redir https://www.verava.ai{uri} permanent
}

# Main website
www.verava.ai {
    root * /var/www/verava
    file_server

    # Try files, then index
    try_files {path} /index.html

    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"
        Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
    }

    # Cache static assets
    @static {
        path *.css *.js *.png *.jpg *.jpeg *.gif *.ico *.svg *.woff *.woff2
    }
    header @static Cache-Control "public, max-age=31536000"
}

# Customer application
app.verava.ai {
    reverse_proxy localhost:4000

    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
        Referrer-Policy "strict-origin-when-cross-origin"
    }
}

# Customer API
api.verava.ai {
    reverse_proxy localhost:4001

    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options "nosniff"
        # CORS for authenticated API
        Access-Control-Allow-Origin "https://app.verava.ai"
        Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
        Access-Control-Allow-Headers "Content-Type, Authorization"
        Access-Control-Allow-Credentials "true"
    }
}

# Documentation
docs.verava.ai {
    root * /var/www/verava-docs
    file_server

    try_files {path} /index.html

    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options "nosniff"
    }
}

Fixed Homelab Caddyfile

Internal reverse proxy for Tailscale-only services.

# =============================================================================
# Fixed Homelab Caddyfile - Internal Services
# Location: Docker VM /etc/caddy/Caddyfile
# Access: Tailscale network only (100.64.0.0/10)
# =============================================================================

{
    email augusto@cronova.dev

    # Use internal CA for Tailscale-only services
    # Or use tailscale cert integration
}

# Home Assistant
jara.cronova.dev {
    reverse_proxy localhost:8123

    # WebSocket support for HA
    @websockets {
        header Connection *Upgrade*
        header Upgrade websocket
    }
    reverse_proxy @websockets localhost:8123
}

# Jellyfin Media Server
yrasema.cronova.dev {
    reverse_proxy localhost:8096

    # Large file uploads for media
    request_body {
        max_size 100GB
    }
}

# Sonarr
japysaka.cronova.dev {
    reverse_proxy localhost:8989
}

# Radarr
taanga.cronova.dev {
    reverse_proxy localhost:7878
}

# Prowlarr
aoao.cronova.dev {
    reverse_proxy localhost:9696
}

# qBittorrent
qbit.cronova.dev {
    reverse_proxy localhost:8080
}

# Pi-hole Admin
dns.cronova.dev {
    reverse_proxy localhost:80
}

NAS Caddyfile (Optional)

For NAS services if you want pretty URLs.

# =============================================================================
# NAS Caddyfile - Storage Services
# Location: NAS /etc/caddy/Caddyfile
# Access: Tailscale network only
# =============================================================================

# Syncthing
sync.cronova.dev {
    reverse_proxy localhost:8384
}

# Frigate NVR
nvr.cronova.dev {
    reverse_proxy localhost:5000

    # WebSocket for live streams
    @websockets {
        header Connection *Upgrade*
        header Upgrade websocket
    }
    reverse_proxy @websockets localhost:5000
}

Docker Compose (VPS)

# docker/vps/networking/caddy/docker-compose.yml

services:
  caddy:
    image: caddy:latest
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"  # HTTP/3
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
      - /var/www:/var/www:ro
    environment:
      - TZ=${TZ:-America/Asuncion}
    networks:
      - caddy-net
      - tailscale-net  # For proxying to homelab

volumes:
  caddy-data:
    name: caddy-data
  caddy-config:
    name: caddy-config

networks:
  caddy-net:
    name: caddy-net
  tailscale-net:
    external: true

Cloudflare DNS Configuration

cronova.dev

Type Name Content Proxy

| A | @ | VPS_IP | Yes | | CNAME | www | cronova.pages.dev | Yes | | CNAME | docs | cronova-docs.pages.dev | Yes | | A | vault | VPS_IP | Yes | | A | status | VPS_IP | Yes | | A | notify | VPS_IP | Yes | | A | api | VPS_IP | Yes | | A | saas | VPS_IP | Yes | | A | hs | RPi5_PUBLIC_IP | No (DNS only) | | A | home | 100.68.63.168 | No (internal) | | A | media | 100.68.63.168 | No (internal) | | A | btc | 100.64.0.11 | No (internal) | | A | nas | 100.82.77.97 | No (internal) | | A | git | 100.64.0.2 | No (internal) |

verava.ai

Type Name Content Proxy

| A | @ | VPS_IP | Yes | | A | www | VPS_IP | Yes | | A | app | VPS_IP | Yes | | A | api | VPS_IP | Yes | | A | docs | VPS_IP | Yes |


SSL/TLS Strategy

Public Services (VPS)

  • Method: Let's Encrypt via Caddy ACME
  • Renewal: Automatic (Caddy handles it)
  • Cloudflare: Full (strict) SSL mode

Internal Services (Tailscale)

# Enable Tailscale HTTPS
tailscale cert jara.cronova.dev

Caddy config for Tailscale certs:

jara.cronova.dev {
    tls /var/lib/tailscale/certs/jara.cronova.dev.crt /var/lib/tailscale/certs/jara.cronova.dev.key
    reverse_proxy localhost:8123
}

Option B: Internal CA

Use Caddy's internal CA for Tailscale-only services (simpler but requires trusting CA on devices).


Security Hardening

Cloudflare Settings

Setting Value

| SSL/TLS | Full (strict) | | Always Use HTTPS | On | | Minimum TLS Version | TLS 1.2 | | Opportunistic Encryption | On | | TLS 1.3 | On | | Automatic HTTPS Rewrites | On |

Caddy Security Headers

All responses include:

  • Strict-Transport-Security (HSTS)
  • X-Content-Type-Options: nosniff
  • X-Frame-Options (DENY or SAMEORIGIN)
  • Referrer-Policy

Rate Limiting (Optional)

# Add to specific routes
api.cronova.dev {
    rate_limit {
        zone api {
            key {remote_host}
            events 100
            window 1m
        }
    }
    reverse_proxy localhost:8080
}

Deployment Checklist

VPS

  • [ ] Install Caddy: apt install caddy
  • [ ] Copy Caddyfile to /etc/caddy/Caddyfile
  • [ ] Create web directories: mkdir -p /var/www/verava /var/www/verava-docs
  • [ ] Validate config: caddy validate --config /etc/caddy/Caddyfile
  • [ ] Reload Caddy: systemctl reload caddy
  • [ ] Test SSL: curl -I <https://status.cronova.dev>

Fixed Homelab

  • [ ] Install Caddy in Docker VM
  • [ ] Configure Tailscale certificates
  • [ ] Copy Caddyfile
  • [ ] Test internal access via Tailscale

Cloudflare

  • [ ] Add all DNS records
  • [ ] Set SSL mode to Full (strict)
  • [ ] Enable "Always Use HTTPS"
  • [ ] Configure page rules if needed

Troubleshooting

Certificate Issues

# Check Caddy logs
journalctl -u caddy -f

# Force certificate renewal
caddy reload --config /etc/caddy/Caddyfile --force

# Test with curl
curl -vI https://status.cronova.dev

502 Bad Gateway

# Check if backend is running
curl localhost:3001  # For Uptime Kuma

# Check Caddy can reach backend
docker exec caddy curl localhost:3001

Tailscale Proxy Issues

# Verify Tailscale connectivity
tailscale ping 100.68.63.168

# Check if service is reachable
curl http://100.68.63.168:8843

Useful Commands

# Validate Caddyfile
caddy validate --config /etc/caddy/Caddyfile

# Reload without downtime
caddy reload --config /etc/caddy/Caddyfile

# Format Caddyfile
caddy fmt --overwrite /etc/caddy/Caddyfile

# View current config
caddy adapt --config /etc/caddy/Caddyfile

# Test certificate
openssl s_client -connect status.cronova.dev:443 -servername status.cronova.dev

References