Securing Your Self-Hosted Services with SSL/TLS Certificates and Let's Encrypt
We earn commissions when you shop through the links on this page, at no additional cost to you. Learn more.
Running self-hosted services without HTTPS is like leaving your front door open. Visitors get security warnings, browsers block connections, and any traffic between your client and server travels unencrypted. I learned this the hard way when I first deployed Nextcloud on my homelab and watched Firefox throw a huge "Your connection is not secure" warning at every user. The fix is free, automated, and honestly takes less than an hour to set up properly.
Let's Encrypt has changed the game. For the first time, anyone—hobbyist or enterprise—can get valid, trusted SSL/TLS certificates at zero cost. Combined with automated renewal tools like Certbot, you can have HTTPS running on multiple self-hosted services with minimal ongoing effort. In this tutorial, I'll walk you through installing certificates, automating renewals, and integrating them into common self-hosting scenarios.
Why SSL/TLS Matters for Self-Hosting
When I access my self-hosted services from outside my home network, HTTPS is non-negotiable. Here's why:
- Data encryption: Passwords, API tokens, and file contents are encrypted in transit. Without HTTPS, anyone on your network (including your ISP) can see everything.
- Browser trust: Modern browsers warn users about unencrypted connections. Green padlock = credibility. Red warning = nobody trusts it.
- Application compatibility: Some web apps (like Nextcloud desktop clients, mobile apps, or third-party integrations) refuse to connect over HTTP.
- Reverse proxy requirements: If you're using Caddy, Traefik, or Nginx Proxy Manager, they expect HTTPS certificates to function properly.
The beauty of Let's Encrypt is that cost is no longer an excuse. In 2025, I run HTTPS on seven self-hosted services—Nextcloud, Immich, Gitea, Vaultwarden, Jellyfin, Uptime Kuma, and a private wiki—and I haven't paid a cent for certificates.
Understanding Let's Encrypt and ACME
Let's Encrypt issues certificates through an automated protocol called ACME (Automated Certificate Management Environment). Here's the flow:
- You request a certificate for your domain (e.g., nextcloud.example.com).
- Let's Encrypt challenges you to prove you own that domain.
- You complete the challenge using one of two methods: HTTP validation or DNS validation.
- Let's Encrypt issues a 90-day certificate.
- An automation tool renews it before expiration.
The HTTP challenge is simpler for most homelabbers: Let's Encrypt hits a special URL on your domain and checks for a temporary token you've created. DNS validation is better if your domain registrar supports API automation, but HTTP works fine for beginners.
Method 1: Setting Up Certificates with Certbot
Certbot is the official Let's Encrypt client. It's simple, well-documented, and works on any Linux system. I prefer it for traditional (non-containerized) setups.
Installation and Initial Setup
Start by installing Certbot and the necessary plugins. On Ubuntu/Debian:
sudo apt update
sudo apt install certbot python3-certbot-nginx python3-certbot-apache -y
If you're using a custom web server (or no web server at all), use the standalone authenticator instead:
sudo apt install certbot -y
Now, request your first certificate. Replace example.com and [email protected] with your actual domain and email:
sudo certbot certonly --standalone -d example.com -d www.example.com -d nextcloud.example.com --email [email protected] --agree-tos --non-interactive
Let me break down that command:
certonly: Request a certificate only (don't modify web server config).--standalone: Use the built-in HTTP server for validation. Certbot temporarily binds to port 80.-d: Add a domain. You can list multiple domains in one request.--email: Let's Encrypt will contact you about expiration (though auto-renewal makes this rare).--agree-tos: Automatically accept Let's Encrypt's terms.--non-interactive: Useful for automation scripts.
Certbot will validate your domains and place certificates in /etc/letsencrypt/live/example.com/. Check them:
sudo ls -la /etc/letsencrypt/live/example.com/
You'll see:
privkey.pem: Your private key (keep this secret).fullchain.pem: Your certificate chain (use this in most web servers).cert.pemandchain.pem: Split versions of fullchain (rarely needed).
Setting Up Auto-Renewal
Certbot certificates expire after 90 days, but Let's Encrypt recommends renewal every 60 days to avoid surprises. The good news: Certbot installs a systemd timer that handles this automatically.
Check the timer:
sudo systemctl status certbot.timer
On most modern systems, this timer is already active. Test renewal without actually renewing:
sudo certbot renew --dry-run
If the dry run succeeds, you're golden. The real renewal will happen automatically.
Integrating with Nginx
If you're using Nginx as a reverse proxy (like I do for several services), point your server block to the certificates:
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Security headers
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://localhost: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;
}
}
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri;
}
After updating your Nginx config, test and reload:
sudo nginx -t
sudo systemctl reload nginx
Method 2: Automated Certificates with Caddy
If you haven't tried Caddy yet, I strongly recommend it. Caddy automates HTTPS entirely—you don't install certificates separately or manage renewal. It's like Certbot is already built in and configured perfectly.
Here's a minimal Caddy config that automatically obtains and renews certificates:
example.com, www.example.com {
reverse_proxy localhost:8080
}
nextcloud.example.com {
reverse_proxy localhost:8081
}
jellyfin.example.com {
reverse_proxy localhost:8096
}
vault.example.com {
reverse_proxy localhost:8000
}
Save this to /etc/caddy/Caddyfile, start the service, and Caddy handles HTTPS completely:
sudo systemctl start caddy
sudo systemctl enable caddy
Caddy automatically requests certificates from Let's Encrypt, stores them in ~/.local/share/caddy, and renews them before expiration. You never touch Certbot or certificate files directly. If you're running Caddy in Docker (which I often do), mount the config volume to persist certificates between restarts.
Hosting on a VPS: A Cost-Effective Option
If you want to avoid port forwarding and dynamic DNS entirely, consider a budget VPS. Providers like RackNerd offer fully managed Linux instances for around $40/year. You get a static IP, reverse DNS setup, and zero ISP throttling concerns. Once on a VPS, SSL/TLS setup is identical to what I've covered here—just do it on a public IP instead of behind NAT.
A VPS is especially worth it if you're running Nextcloud, Vaultwarden, or other services that need reliable public access. The cost is negligible compared to the convenience of not managing port forwarding and firewall rules.
Troubleshooting Common Issues
Certificate Request Fails with "Connection Refused"
Let's Encrypt needs to reach your domain on port 80 (HTTP). Check:
- DNS resolves correctly:
nslookup example.com - Port 80 is open:
sudo ufw allow 80/tcp(or your firewall equivalent). - No other service is using port 80.
- If you're behind NAT, port 80 must forward to your server.
Certificate Works Locally, but HTTPS Errors Remotely
This usually means your domain's DNS is pointing to the wrong IP (likely your ISP's gateway instead of your actual server). Update your DNS A record to your public IP and wait for propagation (usually 15 minutes to 2 hours).
Permission Denied When Reading Certificates
If your application user can't read /etc/letsencrypt/live/, create a post-renewal hook to copy certificates:
sudo mkdir -p /etc/letsencrypt/renewal-hooks/post
sudo cat > /etc/letsencrypt/renewal-hooks/post/copy-certs.sh << 'EOF'
#!/bin/bash
cp /etc/letsencrypt/live/example.com/fullchain.pem /opt/myapp/certs/
cp /etc/letsencrypt/live/example.com/privkey.pem /opt/myapp/certs/
chown myapp:myapp /opt/myapp/certs/*
systemctl restart myapp
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/post/copy-certs.sh
Best Practices and Next Steps
Once certificates are installed, follow these principles:
- Always redirect HTTP to HTTPS. Don't let any service be accessible over unencrypted HTTP. Use the Nginx redirect shown above or Caddy's automatic handling.
- Use strong ciphers. Test your HTTPS configuration with SSL Labs and aim for an A grade.
- Monitor certificate expiration. Even with auto-renewal, it's worth receiving Let's Encrypt's expiry emails. If renewal fails silently, you'll be glad you have that alert.
- Back up your certificates. Include
/etc/letsencrypt/in your regular backup jobs. If you need to migrate to a new server, you can restore existing certificates instead of requesting new ones.
Conclusion
HTTPS on self-hosted services is no longer a luxury—it's the baseline. Let's Encrypt and Certbot (or Caddy's built-in automation) make it free and nearly effortless. If you're running anything more than a local-only homelab, spend an hour this week setting up certificates. Your users will see the green padlock, your applications will work reliably, and you'll sleep better knowing traffic is encrypted.
Start with Caddy if you're deploying new services; it handles everything automatically. Use Certbot if you have existing Nginx or Apache configs. Either way, you'll be more secure, and Let's