Skip to main content
Caddy is the reverse proxy that handles HTTPS ingress for your services in Uncloud. It automatically obtains and renews TLS certificates from Let’s Encrypt, routes traffic to your containers, and provides HTTP/2 and HTTP/3 support out of the box.

How Caddy works in Uncloud

Caddy runs as a service in global mode, meaning one Caddy container runs on every machine in your cluster. Each Caddy instance:
  • Listens on ports 80 (HTTP) and 443 (HTTPS/QUIC)
  • Watches the cluster state for services with published ports
  • Automatically configures routing rules
  • Obtains and manages TLS certificates from Let’s Encrypt
  • Routes traffic to containers across the mesh network
All Caddy containers share the same configuration and TLS certificates through a bind-mounted directory at /var/lib/uncloud/caddy on each machine.

Deploying Caddy

Deploy Caddy across all machines in your cluster:
uc caddy deploy
Example output:
Service: caddy (global mode)
Current image: caddy:2.9.1

Preparing a deployment plan...
Target image: caddy:2.9.1 (latest stable)

Deployment plan:
  Create container caddy-a7f3 on machine oracle-vm
  Create container caddy-b8e2 on machine hetzner-server

Proceed? (y/N): y
[+] Deploying service caddy (global mode) 2/2
 ✔ Container caddy-a7f3 on oracle-vm      Started
 ✔ Container caddy-b8e2 on machine2       Started

Updating cluster domain records in Uncloud DNS...
[+] Verifying internet access to caddy service 2/2
 ✔ Machine oracle-vm (152.67.101.197)     Reachable
 ✔ Machine hetzner-server (5.223.45.199)  Reachable

DNS records updated:
  *.xuw3xd.uncld.dev  A → 152.67.101.197, 5.223.45.199
Caddy is deployed automatically when you:
  • Initialize a cluster with uc machine init (unless you use --no-caddy)
  • Add a machine with uc machine add (unless you use --no-caddy)

Deploy to specific machines

Deploy Caddy to only certain machines:
uc caddy deploy --machine web-1 --machine web-2
This is useful when you have dedicated edge machines for ingress traffic.

Specify a Caddy version

Deploy a specific version of Caddy:
uc caddy deploy --image caddy:2.8.4
By default, Uncloud uses the latest stable version (e.g., caddy:2.9.1).

Caddy commands

View current configuration

Display the current Caddyfile configuration:
uc caddy config
This shows the auto-generated configuration based on your deployed services. The output includes syntax highlighting for readability. To view the configuration from a specific machine:
uc caddy config --machine web-server-1
View raw configuration without syntax highlighting:
uc caddy config --no-color

Custom Caddy configuration

You can add custom global Caddy configuration that gets prepended to the auto-generated config.

Create a custom Caddyfile

Create a file named Caddyfile:
# Global options
{
  email [email protected]
  admin unix//run/caddy/admin.sock
}

# Custom site blocks
example.com {
  reverse_proxy service.internal:8080
  header {
    Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
    X-Frame-Options "SAMEORIGIN"
    X-Content-Type-Options "nosniff"
  }
}

Deploy with custom config

Deploy Caddy with your custom Caddyfile:
uc caddy deploy --caddyfile ./Caddyfile
Your custom configuration will be prepended to the auto-generated configuration. This allows you to:
  • Set global Caddy options
  • Add custom site blocks
  • Configure rate limiting
  • Set up custom headers
  • Add authentication middleware
When you provide a custom Caddyfile, Uncloud:
  1. Reads your Caddyfile content
  2. Generates configuration for your services with published ports
  3. Concatenates your custom config + auto-generated config
  4. Stores the combined config at /var/lib/uncloud/caddy/Caddyfile on each machine
  5. Caddy containers reload with the new configuration
Your custom config is prepended, so it takes precedence for global options and can define additional site blocks that won’t conflict with auto-generated ones.

Automatic TLS certificates

Caddy automatically obtains TLS certificates from Let’s Encrypt for all domains you configure in port mappings.

How it works

  1. You deploy a service with an HTTPS port:
    uc run -p app.example.com:8000/https image/my-app
    
  2. Caddy detects the new service and generates a configuration:
    app.example.com {
      reverse_proxy 10.210.0.5:8000
    }
    
  3. Caddy requests a TLS certificate from Let’s Encrypt using the ACME HTTP challenge
  4. Let’s Encrypt verifies you control the domain by making an HTTP request to http://app.example.com/.well-known/acme-challenge/TOKEN
  5. Caddy receives the certificate and serves your app over HTTPS

Certificate storage

Certificates are stored in /var/lib/uncloud/caddy/data/caddy/certificates/ on each machine. This directory is bind-mounted into the Caddy container. Since Caddy runs on multiple machines, each machine independently obtains certificates. Let’s Encrypt rate limits prevent issues, and Caddy intelligently handles certificate renewal.

Certificate renewal

Caddy automatically renews certificates before they expire (typically 30 days before expiration for Let’s Encrypt’s 90-day certificates). No manual intervention required. You can check certificate expiration:
uc service exec caddy-a7f3 ls -la /data/caddy/certificates/

Rate limits

Let’s Encrypt has rate limits:
  • 50 certificates per registered domain per week
  • 5 duplicate certificates per week
For development, use the staging environment to avoid hitting rate limits. Add this to your custom Caddyfile:
{
  acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}
Remember to remove this for production.

HTTPS port mapping

Publish container ports as HTTPS:
uc run -p app.example.com:8000/https image/my-app
Or in compose.yaml:
services:
  web:
    image: image/my-app
    ports:
      - app.example.com:8000/https
Caddy handles TLS termination and forwards plain HTTP traffic to your container on port 8000.

Multiple domains

Expose a service on multiple domains:
services:
  web:
    image: image/my-app
    ports:
      - app.example.com:8000/https
      - www.example.com:8000/https
      - api.example.com:8000/https
Caddy obtains separate certificates for each domain.

HTTP and HTTPS

Expose both HTTP and HTTPS:
services:
  web:
    image: image/my-app
    ports:
      - app.example.com:8000/http
      - app.example.com:8000/https
Caddy automatically redirects HTTP to HTTPS.

Troubleshooting HTTPS issues

Certificate not obtained

If Caddy can’t obtain a certificate, check:
  1. DNS resolution
    dig app.example.com
    
    Ensure your domain resolves to your machine’s public IP
  2. Port 80 reachability Let’s Encrypt needs to reach port 80 for the HTTP challenge:
    curl http://app.example.com/.well-known/acme-challenge/test
    
  3. Firewall rules Ensure ports 80 and 443 are open on your machine:
    sudo ufw status
    
  4. Caddy logs Check Caddy container logs for errors:
    uc service logs caddy
    

Rate limit errors

If you see errors about rate limits:
failed to obtain certificate: acme: error: 429 :: too many certificates already issued
You’ve hit Let’s Encrypt’s rate limit. Wait until the weekly quota resets or use the staging environment for testing.

Certificate mismatch

If you see certificate warnings in your browser:
  1. Check that DNS points to the correct machine
  2. Clear your browser cache
  3. Verify Caddy obtained the certificate:
    uc caddy config
    
    Look for your domain in the configuration
  4. Check certificate files:
    uc service exec caddy-a7f3 ls /data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/
    

Caddy container not starting

If the Caddy container fails to start:
  1. Check for port conflicts
    sudo netstat -tulpn | grep :80
    sudo netstat -tulpn | grep :443
    
  2. Verify host path exists
    ls -la /var/lib/uncloud/caddy
    
  3. Check Caddy logs
    uc service logs caddy
    
  4. Inspect service state
    uc service inspect caddy
    

Advanced configuration

Custom error pages

Add custom error pages in your Caddyfile:
(error_pages) {
  handle_errors {
    rewrite * /{http.error.status_code}.html
    file_server
  }
}

app.example.com {
  reverse_proxy service.internal:8000
  import error_pages
}

Rate limiting

Limit requests per IP:
app.example.com {
  rate_limit {
    zone dynamic {
      key {remote_host}
      events 100
      window 1m
    }
  }
  reverse_proxy service.internal:8000
}

Basic authentication

Protect a site with basic auth:
admin.example.com {
  basicauth {
    alice $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG
  }
  reverse_proxy service.internal:8000
}
Generate the password hash:
uc service exec caddy-a7f3 caddy hash-password

Path-based routing

Route different paths to different services:
app.example.com {
  handle /api/* {
    reverse_proxy api-service.internal:8000
  }
  handle /admin/* {
    reverse_proxy admin-service.internal:8001
  }
  handle {
    reverse_proxy web-service.internal:8080
  }
}

Caddy service details

The Caddy service deployed by Uncloud has these characteristics:
  • Service name: caddy
  • Mode: Global (one container per machine)
  • Image: caddy:2.X.X (latest stable)
  • Ports:
    • 80/tcp - HTTP
    • 443/tcp - HTTPS
    • 443/udp - HTTP/3 (QUIC)
  • Volumes:
    • /var/lib/uncloud/caddy/config and /data (TLS certificates and config)
    • /run/uncloud/caddy/run/caddy (Unix socket for admin API)
  • Command: caddy run -c /config/Caddyfile
  • Environment: CADDY_ADMIN=unix//run/caddy/admin.sock

Caddy command reference

CommandDescription
uc caddy deployDeploy or upgrade Caddy across machines
uc caddy deploy --image IMAGEDeploy a specific Caddy version
uc caddy deploy --caddyfile PATHDeploy with custom Caddyfile
uc caddy deploy --machine MACHINEDeploy only to specific machines
uc caddy configView current Caddyfile configuration
uc caddy config --machine MACHINEView config from a specific machine
uc caddy config --no-colorView config without syntax highlighting

Next steps

DNS Management

Set up custom domains and DNS records

Monitoring

Monitor service health and logs

Build docs developers (and LLMs) love