Automating SSL Certificates for Self-Hosted Services with Let's Encrypt
We earn commissions when you shop through the links on this page, at no additional cost to you. Learn more.
If you've ever logged into a self-hosted service and been greeted by a terrifying browser warning about an insecure connection, you already know why proper SSL matters. Let's Encrypt changed the game by making trusted certificates completely free, but the real magic is in automating the whole renewal process so you never have to think about it again. In this tutorial I'll walk you through two battle-tested approaches — Caddy (my preference for most setups) and Certbot with a cron job — so you can pick the right tool for your stack.
Why Automation Matters: The 90-Day Expiry Problem
Let's Encrypt certificates expire after 90 days, by design. That short window is meant to encourage automation and limit the damage from compromised keys. The problem is that if you issue your first certificate manually and then forget about it, you will wake up one morning to find that Nextcloud, Vaultwarden, Gitea, or whatever you're running has gone HTTPS-dark. Your browser will refuse to load it without a scary click-through, and some clients (like mobile apps and API consumers) won't connect at all.
The good news: fully automated renewal is straightforward, and once it's in place you genuinely never think about it again. I've had the same Caddy setup running across four subdomains for over a year without touching a certificate once.
Method 1: Caddy (My Recommended Approach)
I prefer Caddy because it handles everything — ACME challenge, certificate storage, renewal, and HTTPS redirection — with almost zero configuration. There's no separate renewal daemon to babysit, no cron jobs, no systemd timers. Caddy obtains a certificate when it first starts and silently renews it 30 days before expiry. For most self-hosters, this is the right answer.
Assuming you're on a Debian/Ubuntu VPS or home server, here's how to get Caddy installed and serving HTTPS for a service like Vaultwarden running locally on port 8080:
# Install Caddy via the official repo
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
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 && sudo apt install caddy
# Verify it's running
sudo systemctl status caddy
Now edit your Caddyfile at /etc/caddy/Caddyfile. Replace vault.yourdomain.com with your actual subdomain:
vault.yourdomain.com {
reverse_proxy localhost:8080
}
gitea.yourdomain.com {
reverse_proxy localhost:3000
}
jellyfin.yourdomain.com {
reverse_proxy localhost:8096
}
That's genuinely the entire config for three services with full HTTPS. Caddy will contact Let's Encrypt's ACME endpoint, complete the HTTP-01 challenge automatically (it temporarily serves a file on port 80), store the certificate in /var/lib/caddy/.local/share/caddy/certificates/, and set up a renewal loop in the background. Reload the config with:
sudo systemctl reload caddy
# Watch the journal to confirm certificate issuance
sudo journalctl -u caddy -f
You should see lines like certificate obtained successfully within about 30 seconds of the reload, assuming your DNS is pointing at the server and ports 80 and 443 are open.
Method 2: Certbot with Automated Renewal
If you're already using Nginx or another reverse proxy and don't want to switch to Caddy, Certbot is the standard tool. It's more hands-on than Caddy but still fully automatable. I use this on a couple of older setups that I haven't migrated yet.
Install Certbot and the Nginx plugin, then obtain certificates for your domains:
# Install Certbot on Debian/Ubuntu
sudo apt update
sudo apt install -y certbot python3-certbot-nginx
# Obtain a certificate (Certbot will edit your Nginx config automatically)
sudo certbot --nginx -d nextcloud.yourdomain.com -d immich.yourdomain.com
# Or use standalone mode if you're not using Nginx
# (stop whatever is on port 80 first)
sudo certbot certonly --standalone -d uptime.yourdomain.com
# Test the renewal process without actually renewing
sudo certbot renew --dry-run
When you install Certbot via apt on modern Debian/Ubuntu systems, it drops a systemd timer at /lib/systemd/system/certbot.timer that runs twice a day and calls certbot renew. Certbot only actually renews when a certificate is within 30 days of expiry, so running it frequently is harmless. Check that the timer is active:
sudo systemctl list-timers | grep certbot
# You should see certbot.timer listed as active
# If it's not active, enable it manually
sudo systemctl enable --now certbot.timer
After renewal, Nginx needs to reload to pick up the new certificate. Certbot handles this with deploy hooks. Create a hook file that reloads Nginx automatically:
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'EOF'
#!/bin/bash
systemctl reload nginx
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
This script runs every time Certbot successfully renews a certificate, keeping your Nginx process in sync with the latest cert without any manual intervention.
Wildcard Certificates with DNS-01 Challenge
If you run more than a handful of subdomains or you're issuing certificates for internal services that aren't publicly reachable on port 80, wildcard certificates via the DNS-01 challenge are the way to go. A single certificate for *.yourdomain.com covers every subdomain.
The catch: DNS-01 requires you to create a TXT record in your DNS provider's API. Most major providers (Cloudflare, DigitalOcean, Route53) have Certbot plugins for this. Here's the Cloudflare example, which I use for my internal homelab services:
# Install the Cloudflare plugin
sudo apt install python3-certbot-dns-cloudflare
# Create a credentials file (keep this protected)
sudo mkdir -p /etc/letsencrypt/secrets
sudo tee /etc/letsencrypt/secrets/cloudflare.ini <<'EOF'
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN_HERE
EOF
sudo chmod 600 /etc/letsencrypt/secrets/cloudflare.ini
# Issue a wildcard certificate
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/secrets/cloudflare.ini \
-d "yourdomain.com" \
-d "*.yourdomain.com"
The resulting certificate covers every subdomain you can think of: git.yourdomain.com, photos.yourdomain.com, ai.yourdomain.com — all with one cert, and all auto-renewed by the same systemd timer.
--staging flag during testing to get a non-trusted test certificate that doesn't count against your limits. Remove the flag only when you're ready for production.Hosting Your Services on a VPS
A lot of this becomes simpler when your services are already on a VPS with a static IP and proper DNS. If you're still running things locally with dynamic DNS or port-forwarding gymnastics, a cheap Droplet can make the certificate story much cleaner. DigitalOcean Droplets give you dependable uptime with a 99.99% SLA and predictable monthly pricing — a $6/month Droplet is more than enough to run Caddy with half a dozen self-hosted apps behind it.
Verifying Everything Is Working
Once you've got certificates issued and renewal configured, verify the setup with a few quick checks:
# Check certificate expiry and issuer from the command line
echo | openssl s_client -connect yourdomain.com:443 2>/dev/null \
| openssl x509 -noout -issuer -dates
# List all Certbot-managed certificates and their expiry
sudo certbot certificates
# For Caddy, check stored certificate info
sudo ls -la /var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/
I also run Uptime Kuma and add an SSL expiry monitor for each of my domains. It alerts me via Telegram if any certificate is less than 14 days from expiry — which should never happen with proper automation, but it's a great safety net to have.
Conclusion
SSL automation with Let's Encrypt isn't optional anymore — it's table stakes for any serious self-hosted setup. My recommendation: if you're starting fresh or can tolerate switching reverse proxies, use Caddy and enjoy zero-configuration certificate management. If you're locked into Nginx or Apache, Certbot with the systemd timer and a deploy hook is rock-solid. Either way, the goal is the same: set it up once and forget about it.
Next steps: if you're handling multiple services with a single reverse proxy, check out our tutorials on setting up Traefik as a reverse proxy for Docker containers or adding Authelia for SSO and MFA in front of your freshly secured services.
Discussion