Setting Up SSL/TLS Certificates with Let's Encrypt and Certbot for Self-Hosted Services

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:

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:

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.

Tip: Use 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.).

Watch out: Store your DNS API credentials securely. Certbot uses them only for validation, but they grant access to your DNS records. Use restricted API tokens, not your master credentials.

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:

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.