Setting Up SSL/TLS Certificates with Let's Encrypt and Certbot for Self-Hosted Services
When I first self-hosted Nextcloud on my homelab, I was running everything over HTTP. My browser screamed warnings, my phone wouldn't trust the certificate, and anyone could theoretically intercept my traffic. That changed when I discovered Let's Encrypt and Certbot—and suddenly securing my entire homelab became trivial and free.
Whether you're running services on a cheap VPS (you can grab one from providers like RackNerd for around $40/year), a home server, or a Docker-based stack, securing your self-hosted applications with proper SSL/TLS certificates isn't optional anymore. Modern browsers punish unencrypted services, and if you're accessing your homelab remotely, encryption is non-negotiable. Let me walk you through exactly how I set this up and keep it running.
Why Let's Encrypt and Certbot?
Before diving into the how, let me be clear: I used to buy certificates. I paid money annually for the privilege of encryption. Then Let's Encrypt arrived in 2015 and made that completely unnecessary. Their certificates are free, trusted by every major browser, and valid for 90 days.
Certbot is the official ACME client from the Electronic Frontier Foundation. It automates the entire certificate lifecycle—proving domain ownership, installing the certificate, configuring your web server, and most importantly, automatically renewing before expiry. I've been using this stack for five years across dozens of services without a single expired certificate.
The combination is unbeatable: zero cost, automatic renewal, minimal configuration, and bulletproof trust across all devices.
Prerequisites and Setup
You'll need a few things before starting:
- A domain name you control (DNS access required)
- A server running Linux (Ubuntu, Debian, CentOS—I prefer Ubuntu)
- A web server already running (Nginx, Apache, or Caddy)
- Port 80 (HTTP) and 443 (HTTPS) accessible from the internet
- Root or sudo access on your server
If you don't have a public VPS yet, I recommend starting with something minimal. A $40/year VPS gives you enough resources to run a handful of self-hosted services comfortably. From there, the setup is identical whether you're on a $5/month DigitalOcean droplet or an old laptop at home.
Installing Certbot
On Ubuntu/Debian, installation is straightforward:
sudo apt-get update
sudo apt-get install -y certbot python3-certbot-nginx python3-certbot-apache
# Verify installation
certbot --version
I'm installing the Nginx and Apache plugins because I use both. If you're running Caddy or another reverse proxy, you can still use Certbot's standalone mode or the DNS plugin.
For other distros, check the official Certbot documentation for specific instructions.
Getting Your First Certificate
Let's assume you have Nginx running and a domain like homelab.example.com. Certbot can automatically configure Nginx for you:
sudo certbot certonly --nginx -d homelab.example.com -d www.homelab.example.com --agree-tos --no-eff-email --email [email protected]
Breaking this down:
--nginx: Use the Nginx plugin for verification-d homelab.example.com: The domain you're requesting a certificate for (can specify multiple)--agree-tos: Agree to Let's Encrypt's terms automatically--no-eff-email: Don't share your email with the EFF--email: Contact email for certificate expiry warnings
Certbot will validate that you control the domain by placing a verification file at /.well-known/acme-challenge/ on your web server. Once verified, your certificate is ready in about 10 seconds.
certonly instead of certify if you want to manage your web server config yourself. This gives you finer control and prevents accidental configuration breaks.Configuring Nginx to Use Your Certificate
Once Certbot has your certificate, you need to configure Nginx to actually use it. Here's what my standard configuration looks like:
server {
listen 80;
listen [::]:80;
server_name homelab.example.com www.homelab.example.com;
# Redirect all HTTP traffic to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name homelab.example.com www.homelab.example.com;
# SSL certificate paths (generated by Certbot)
ssl_certificate /etc/letsencrypt/live/homelab.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/homelab.example.com/privkey.pem;
# Modern SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
After saving this configuration, test and reload Nginx:
sudo nginx -t
sudo systemctl reload nginx
Visit your domain in a browser—you should now see a green lock icon and no security warnings.
Automatic Renewal with Systemd
This is the beautiful part. Certbot certificates last 90 days, but they renew automatically. Certbot installs a systemd timer that checks for expiring certificates twice daily:
sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer
# Check status
sudo systemctl status certbot.timer
sudo systemctl list-timers certbot.timer
When a certificate is within 30 days of expiry, the timer runs certbot renew automatically. The renewal process is silent—if it succeeds, nothing happens. If it fails, you'll receive an email at the address you provided.
I've never had to manually renew a certificate in five years of running this. It just works.
Using Certbot with Docker and Reverse Proxies
If you're running Docker, you have two approaches:
Option 1: Host Certbot on the Host Machine
Run Certbot on your host, store certificates in a volume, and mount them into your container. This is what I prefer because it separates concerns—certificate management happens once, independent of container restarts.
Option 2: DNS Challenge for Wildcard Certificates
If you have multiple subdomains (like nextcloud.homelab.com, jellyfin.homelab.com, etc.), the DNS challenge is more efficient. You get a single wildcard certificate valid for *.homelab.com.
sudo certbot certonly --dns-cloudflare -d homelab.com -d '*.homelab.com' \
--agree-tos --no-eff-email --email [email protected]
For this, you'll need the Cloudflare DNS plugin and API credentials. Other DNS providers have their own plugins (AWS Route53, DigitalOcean, etc.).
Handling Renewal with Docker
If Certbot runs on your host and your services run in Docker, you need to tell them when certificates renew. I use a post-renewal hook:
sudo nano /etc/letsencrypt/renewal-hooks/post/docker-reload.sh
Add this script:
#!/bin/bash
# Post-renewal hook to reload Docker services
docker-compose -f /opt/services/docker-compose.yml restart nginx
docker-compose -f /opt/services/docker-compose.yml restart nextcloud
docker exec adguard-home /usr/bin/adguardhome -s reload 2>/dev/null || true
echo "Certificate renewal complete at $(date)" >> /var/log/certbot-renewal.log
Make it executable and Certbot will run it automatically after successful renewal:
sudo chmod +x /etc/letsencrypt/renewal-hooks/post/docker-reload.sh
Securing Certbot Itself
A few hardening steps I always take:
- Restrict permissions:
sudo chmod 600 /etc/letsencrypt/renewal-hooks/post/*.sh - Monitor certificate expiry: Set a calendar reminder or use Uptime Kuma to alert if your cert is expiring (belt-and-suspenders approach)
- Keep Certbot updated:
sudo apt-get update && apt-get upgrade certbot python3-certbot-* - Test renewal before it matters:
sudo certbot renew --dry-runsimulates the renewal process without actually issuing a new certificate
Common Gotchas and Solutions
Port 80 Not Accessible: Certbot's standalone mode requires port 80 to be reachable from the internet. If it's blocked, use the DNS challenge instead.
Rate Limits: Let's Encrypt has rate limits (50 certificates per domain per week). If you're testing heavily, use the staging environment: --staging flag. Staging certificates won't be trusted by browsers, but they're perfect for testing.
Renewal Failures on Renewal Hooks: If a hook script fails, the certificate still renews, but services might not reload. Check logs: journalctl -u certbot.service -n 50
Certificate Not Found After Install: Certbot places certificates in /etc/letsencrypt/live/domain.com/ with symbolic links. Don't copy the certs—always reference them via the symlinks, which update automatically on renewal.
Monitoring and Alerts
Even with automatic renewal, I like knowing when certificates are about to expire. Add this to your monitoring stack:
#!/bin/bash
# Check certificate expiry - run daily via cron
DOMAIN="homelab.example.com"
CERT_FILE="/etc/letsencrypt/live/$DOMAIN/cert.pem"
if [ ! -f "$CERT_FILE" ]; then
echo "Certificate file not found: $CERT_FILE"
exit 1
fi
EXPIRY=$(openssl x509 -enddate -noout -in "$CERT_FILE" | cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s)
CURRENT_EPOCH=$(date +%s)
DAYS_LEFT=$(( ($EXPIRY_EPOCH - $CURRENT_EPOCH) / 86400 ))
echo "Certificate for $DOMAIN expires in $DAYS_LEFT days"
if [ "$DAYS_LEFT" -lt 14 ]; then
# Send alert (webhook, email, etc.)
echo "WARNING: Certificate expiring soon" > /dev/stderr
fi
Next Steps and Beyond
Once your basic setup is running, consider these next moves:
Implement HSTS: I already showed this in the Nginx config, but ensure Strict-Transport-Security headers are set. Browsers remember this and force HTTPS even if a user tries HTTP.
Test Your Setup: Use SSL Labs to audit your configuration. You should score an A or A+ for proper security.
Automate Certificate Deployment Across Multiple Domains: If you manage 10+ domains, script the entire renewal and reload process with a custom tool or use a reverse proxy like Traefik that handles Certbot natively.