Setup Caddy automatic SSL certificates with Let's Encrypt and DNS challenges

Intermediate 25 min Jun 02, 2026 152 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Configure Caddy 2 web server with automatic SSL certificate provisioning using Let's Encrypt and DNS challenge authentication for secure HTTPS automation.

Prerequisites

  • Root or sudo access
  • Domain with DNS provider API access
  • DNS provider API credentials

What this solves

Caddy 2 provides automatic HTTPS with zero configuration, but DNS challenges are essential when your server isn't publicly accessible or you need wildcard certificates. This tutorial configures Caddy with Let's Encrypt DNS challenges for automatic SSL certificate provisioning and renewal.

Step-by-step installation

Update system packages

Start by updating your package manager to ensure you get the latest versions available.

sudo apt update && sudo apt upgrade -y
sudo dnf update -y

Install required packages

Install curl and other essential tools needed for Caddy installation and DNS provider configuration.

sudo apt install -y curl wget gnupg lsb-release
sudo dnf install -y curl wget gnupg

Add Caddy repository

Add the official Caddy repository to get the latest stable version with automatic updates.

curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
dnf install 'dnf-command(copr)' -y
sudo dnf copr enable @caddy/caddy -y

Install Caddy 2

Install Caddy 2 from the official repository to get the latest features and security updates.

sudo apt install -y caddy
sudo dnf install -y caddy

Create Caddy configuration directory

Create the necessary directories for Caddy configuration and ensure proper ownership for the caddy user.

sudo mkdir -p /etc/caddy
sudo chown -R caddy:caddy /etc/caddy
sudo chmod 755 /etc/caddy

Configure Caddy with DNS challenge

Create the main Caddy configuration file with DNS challenge settings for your DNS provider. This example uses Cloudflare.

{
    email admin@example.com
    acme_ca https://acme-v02.api.letsencrypt.org/directory
}

example.com {
    tls {
        dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    }
    
    root * /var/www/html
    file_server
    
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        X-XSS-Protection "1; mode=block"
        Referrer-Policy "strict-origin-when-cross-origin"
    }
    
    log {
        output file /var/log/caddy/access.log
        format json
    }
}

*.example.com {
    tls {
        dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    }
    
    @api host api.example.com
    handle @api {
        reverse_proxy localhost:3000
    }
    
    @app host app.example.com
    handle @app {
        reverse_proxy localhost:8080
    }
    
    handle {
        respond "Subdomain not configured" 404
    }
}

Create DNS provider environment file

Create a secure environment file containing your DNS provider credentials. Never store these in the main configuration file.

sudo touch /etc/caddy/caddy.env
sudo chown caddy:caddy /etc/caddy/caddy.env
sudo chmod 600 /etc/caddy/caddy.env
# Cloudflare API Token
CLOUDFLARE_API_TOKEN=your_cloudflare_api_token_here

Alternative DNS providers:

ROUTE53_ACCESS_KEY_ID=your_key_id

ROUTE53_SECRET_ACCESS_KEY=your_secret_key

ROUTE53_REGION=us-east-1

DIGITALOCEAN_TOKEN=your_do_token

NAMECHEAP_API_USER=your_username

NAMECHEAP_API_KEY=your_api_key

Create web root directory

Create the web root directory and set proper permissions for serving static content.

sudo mkdir -p /var/www/html
sudo chown -R caddy:caddy /var/www/html
sudo chmod 755 /var/www/html

Create log directory

Create the log directory with proper permissions for Caddy access and error logs.

sudo mkdir -p /var/log/caddy
sudo chown -R caddy:caddy /var/log/caddy
sudo chmod 755 /var/log/caddy

Create systemd override for environment variables

Configure systemd to load the DNS provider credentials securely without exposing them in process lists.

sudo mkdir -p /etc/systemd/system/caddy.service.d
[Service]
EnvironmentFile=/etc/caddy/caddy.env
Restart=always
RestartSec=5
TimeoutStopSec=30

Security hardening

NoNewPrivileges=yes PrivateTmp=yes PrivateDevices=yes ProtectHome=yes ProtectSystem=strict ReadWritePaths=/var/lib/caddy /var/log/caddy /var/www CapabilityBoundingSet=CAP_NET_BIND_SERVICE AmbientCapabilities=CAP_NET_BIND_SERVICE

Test Caddy configuration

Validate your Caddy configuration file before starting the service to catch syntax errors early.

sudo caddy validate --config /etc/caddy/Caddyfile

Reload systemd and enable Caddy

Reload systemd to apply the override configuration and enable Caddy to start automatically on boot.

sudo systemctl daemon-reload
sudo systemctl enable caddy
sudo systemctl start caddy

Configure DNS challenge providers

Cloudflare DNS challenge

Create a Cloudflare API token with Zone:Zone:Read and Zone:DNS:Edit permissions for your domain.

example.com {
    tls {
        dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    }
    # Your site configuration
}

AWS Route53 DNS challenge

Configure Route53 with IAM credentials that have Route53 permissions for your hosted zone.

example.com {
    tls {
        dns route53 {
            access_key_id {env.ROUTE53_ACCESS_KEY_ID}
            secret_access_key {env.ROUTE53_SECRET_ACCESS_KEY}
            region {env.ROUTE53_REGION}
        }
    }
    # Your site configuration
}

DigitalOcean DNS challenge

Use your DigitalOcean personal access token with read and write permissions for DNS records.

example.com {
    tls {
        dns digitalocean {env.DIGITALOCEAN_TOKEN}
    }
    # Your site configuration
}

Advanced SSL configuration

Configure custom ACME server

Use a custom ACME server like your own Boulder instance or a different certificate authority.

{
    email admin@example.com
    acme_ca https://your-acme-server.com/acme/directory
    acme_ca_root /path/to/custom-ca-cert.pem
}

example.com {
    tls {
        dns cloudflare {env.CLOUDFLARE_API_TOKEN}
        ca https://your-acme-server.com/acme/directory
    }
    # Your site configuration
}

Configure certificate lifetime and renewal

Customize certificate renewal thresholds and configure email notifications for certificate events.

{
    email admin@example.com
    cert_lifetime 90d
    renew_interval 30d
    
    log default {
        output file /var/log/caddy/caddy.log
        level INFO
        format json
    }
}

example.com {
    tls {
        dns cloudflare {env.CLOUDFLARE_API_TOKEN}
        lifetime 90d
        must_staple
    }
    # Your site configuration
}

Configure multiple DNS providers

Use different DNS providers for different domains when managing multiple zones across providers.

example.com {
    tls {
        dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    }
    # Configuration for Cloudflare domain
}

example.net {
    tls {
        dns route53 {
            access_key_id {env.ROUTE53_ACCESS_KEY_ID}
            secret_access_key {env.ROUTE53_SECRET_ACCESS_KEY}
            region us-east-1
        }
    }
    # Configuration for Route53 domain
}

example.org {
    tls {
        dns digitalocean {env.DIGITALOCEAN_TOKEN}
    }
    # Configuration for DigitalOcean domain
}

Security hardening

Configure firewall rules

Open only the necessary ports for HTTP and HTTPS traffic while blocking all other incoming connections.

sudo ufw enable
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status verbose
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
sudo firewall-cmd --list-all

Configure log rotation

Set up automatic log rotation to prevent Caddy logs from consuming all available disk space.

/var/log/caddy/*.log {
    daily
    missingok
    rotate 52
    compress
    delaycompress
    notifempty
    sharedscripts
    postrotate
        systemctl reload caddy > /dev/null 2>&1 || true
    endscript
}

Set up monitoring and alerting

Configure basic monitoring to track certificate expiration and service health.

# Check certificate expiration daily at 2 AM
0 2   * caddy /usr/bin/caddy list-certificates --config /etc/caddy/Caddyfile | grep -E "expires|error" | mail -s "Caddy Certificate Status" admin@example.com

Verify your setup

Check that Caddy is running properly and certificates are being issued automatically.

sudo systemctl status caddy
sudo caddy list-certificates --config /etc/caddy/Caddyfile
curl -I https://example.com
openssl s_client -connect example.com:443 -servername example.com < /dev/null | openssl x509 -text -noout | grep -A2 "Validity"

Check the Caddy logs for any DNS challenge errors or certificate provisioning issues.

sudo journalctl -u caddy -f --no-pager
sudo tail -f /var/log/caddy/caddy.log

Test that your DNS challenges work by forcing a certificate renewal.

sudo caddy reload --config /etc/caddy/Caddyfile

Common issues

SymptomCauseFix
DNS challenge failsInvalid API token or insufficient permissionsVerify token permissions and test with DNS provider API
Certificate not issuedDomain not pointing to server or DNS propagation delayWait for DNS propagation, verify A/AAAA records
Permission denied errorsIncorrect file ownership or directory permissionssudo chown -R caddy:caddy /etc/caddy /var/log/caddy
Caddy won't startConfiguration syntax errorsudo caddy validate --config /etc/caddy/Caddyfile
Environment variables not loadedsystemd override not appliedsudo systemctl daemon-reload && sudo systemctl restart caddy
Wildcard certificates not workingDNS provider doesn't support wildcards or incorrect configurationVerify provider supports wildcards, check DNS plugin documentation
Never use chmod 777. It gives every user on the system full access to your files. Instead, fix ownership with chown and use minimal permissions like 644 for files and 755 for directories.

Next steps

Running this in production?

Want this handled for you? Setting this up once is straightforward. Keeping it patched, monitored, backed up and performant across environments is the harder part. See how we run infrastructure like this for European teams.

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle managed cloud infrastructure for businesses that depend on uptime. From initial setup to ongoing operations.