Skip to main content
Caddy is a modern HTTP server with automatic HTTPS. This guide shows how to deploy Anubis behind Caddy.

Basic Configuration

Caddy makes it simple to set up a reverse proxy with automatic TLS. Here’s a minimal Caddyfile:
example.com {
  reverse_proxy anubis:3000 {
    header_up X-Real-IP {remote_host}
  }
}
Caddy automatically:
  • Obtains and renews TLS certificates via Let’s Encrypt
  • Redirects HTTP to HTTPS
  • Sets HTTP/2 and HTTP/3

Complete Example

Here’s a production-ready setup with all recommended headers: Caddyfile:
:80 {
  reverse_proxy http://anubis:3000 {
    header_up X-Real-IP {remote_host}
    header_up X-Http-Version {http.request.proto}
  }
}

:443 {
  tls /etc/certs/cert.pem /etc/certs/key.pem
  
  reverse_proxy http://anubis:3000 {
    header_up X-Real-IP {remote_host}
    header_up X-Http-Version {http.request.proto}
    header_up X-Tls-Version {http.request.tls.version}
  }
}
docker-compose.yml:
services:
  caddy:
    image: caddy:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./certs:/etc/certs:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - anubis

  anubis:
    image: ghcr.io/techarohq/anubis:main
    environment:
      BIND: ":3000"
      TARGET: "http://backend:8080"
      POLICY_FNAME: "/etc/techaro/anubis/policy.yaml"
    volumes:
      - ./anubis-config:/etc/techaro/anubis

  backend:
    image: your-app:latest
    expose:
      - "8080"

volumes:
  caddy_data:
  caddy_config:

Automatic HTTPS

For automatic HTTPS with Let’s Encrypt:
example.com {
  reverse_proxy anubis:3000 {
    header_up X-Real-IP {remote_host}
    header_up X-Forwarded-For {remote_host}
  }
}
Caddy automatically:
  1. Obtains a TLS certificate from Let’s Encrypt
  2. Renews certificates before expiration
  3. Redirects HTTP to HTTPS
No additional configuration needed!

Custom TLS Certificates

To use your own certificates:
example.com {
  tls /path/to/cert.pem /path/to/key.pem
  
  reverse_proxy anubis:3000 {
    header_up X-Real-IP {remote_host}
  }
}

Multiple Backends

Protect multiple applications with one Anubis instance:
# Protected site 1
site1.example.com {
  reverse_proxy anubis:3000
}

# Protected site 2
site2.example.com {
  reverse_proxy anubis:3000
}

# Unprotected site
unprotected.example.com {
  reverse_proxy backend:8080
}
Configure Anubis to accept multiple domains:
anubis --cookie-domain example.com \
       --redirect-domains site1.example.com,site2.example.com \
       --bind :3000

Path-Based Routing

Protect only specific paths:
example.com {
  # Protected admin area
  reverse_proxy /admin/* anubis:3000
  
  # Public content goes directly to backend
  reverse_proxy backend:8080
}
Note: This requires configuring Anubis with --base-prefix /admin.

Header Configuration

Caddy automatically sets several headers, but you can customize them:
example.com {
  reverse_proxy anubis:3000 {
    # Client IP
    header_up X-Real-IP {remote_host}
    header_up X-Forwarded-For {remote_host}
    
    # Protocol information
    header_up X-Forwarded-Proto {scheme}
    header_up X-Forwarded-Host {host}
    
    # TLS information
    header_up X-Tls-Version {http.request.tls.version}
    header_up X-Tls-Cipher {http.request.tls.cipher_suite}
  }
}

Placeholders

Caddy provides many useful placeholders:
  • {remote_host} - Client IP address
  • {scheme} - http or https
  • {host} - Host header value
  • {method} - HTTP method
  • {uri} - Request URI
  • {http.request.proto} - HTTP version
  • {http.request.tls.version} - TLS version
  • {http.request.tls.cipher_suite} - TLS cipher suite
See Caddy placeholders for the complete list.

Health Checks

Caddy can perform active health checks on Anubis:
example.com {
  reverse_proxy anubis:3000 {
    header_up X-Real-IP {remote_host}
    
    health_uri /healthz
    health_port 9090
    health_interval 10s
    health_timeout 5s
  }
}
This checks Anubis metrics endpoint every 10 seconds.

Load Balancing

Run multiple Anubis instances behind Caddy:
example.com {
  reverse_proxy anubis1:3000 anubis2:3000 anubis3:3000 {
    header_up X-Real-IP {remote_host}
    
    lb_policy round_robin
    lb_try_duration 5s
    
    health_uri /healthz
    health_port 9090
  }
}
Important: All Anubis instances must share the same signing key:
# Generate key once
openssl rand -hex 32 > anubis-key.txt

# Use on all instances
anubis --ed25519-private-key-hex-file /secrets/anubis-key.txt

Logging

Customize access logs:
example.com {
  log {
    output file /var/log/caddy/access.log {
      roll_size 100mb
      roll_keep 10
    }
    format json
  }
  
  reverse_proxy anubis:3000 {
    header_up X-Real-IP {remote_host}
  }
}

Advanced Configuration

Custom Error Pages

example.com {
  handle_errors {
    @502 `{http.error.status_code} == 502`
    handle @502 {
      respond "Service temporarily unavailable" 502
    }
  }
  
  reverse_proxy anubis:3000 {
    header_up X-Real-IP {remote_host}
  }
}

Rate Limiting

Caddy can rate limit before reaching Anubis:
example.com {
  rate_limit {
    zone example_zone 1m 100r/s
  }
  
  reverse_proxy anubis:3000 {
    header_up X-Real-IP {remote_host}
  }
}
Note: This requires the caddy-ratelimit plugin.

Request Buffering

example.com {
  reverse_proxy anubis:3000 {
    header_up X-Real-IP {remote_host}
    
    # Buffer requests up to 10MB
    flush_interval -1
    request_buffers 10MB
  }
}

Testing

Test your Caddy configuration:
# Validate Caddyfile syntax
caddy validate --config Caddyfile

# Test with curl
curl -v https://example.com

# Check headers received by Anubis
curl -H "X-Forwarded-For: 1.2.3.4" http://localhost:3000/

Troubleshooting

Certificate Issues

If Let’s Encrypt certificates fail:
# Check Caddy logs
docker logs caddy

# Ensure port 80 is accessible for ACME challenge
curl http://example.com/.well-known/acme-challenge/test

IP Address Not Detected

Verify Anubis receives the correct IP:
  1. Don’t use --use-remote-address when behind Caddy
  2. Ensure header_up X-Real-IP {remote_host} is set
  3. Check Anubis logs for received IP

Connection Refused

Check that:
  1. Anubis is running: docker ps
  2. Anubis is listening on correct port
  3. Network connectivity: docker exec caddy wget -O- http://anubis:3000/

Resources

Build docs developers (and LLMs) love