Securing Your Self-Hosted Services with SSL/TLS Certificates and Let's Encrypt

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:

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:

  1. You request a certificate for your domain (e.g., nextcloud.example.com).
  2. Let's Encrypt challenges you to prove you own that domain.
  3. You complete the challenge using one of two methods: HTTP validation or DNS validation.
  4. Let's Encrypt issues a 90-day certificate.
  5. 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:

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:

Watch out: The /etc/letsencrypt/live directory is owned by root. If your service runs as a different user (common in Docker or systemd services), make sure that user can read the certificate files. You'll likely need to adjust permissions or use a post-renewal hook to copy certificates to an accessible location.

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.

Tip: Caddy's simplicity is a game-changer if you're managing multiple self-hosted services. One config file, no certificate management, and automatic HTTPS on every service. That's why I've moved all my homelab reverse proxies to Caddy over the past year.

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:

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:

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