SSL Certificates for Self-Hosted Services: Complete Guide to HTTPS

Running self-hosted services over plain HTTP is risky. Passwords, session cookies, and personal data travel in cleartext, vulnerable to interception.

HTTPS encrypts everything. With free SSL certificates from Let’s Encrypt, there’s no excuse to skip it.

This guide covers three ways to get SSL certificates for your home server:

  1. Nginx Proxy Manager - Easiest, GUI-based
  2. Caddy - Automatic HTTPS, zero config
  3. Certbot - Manual, maximum control

Why SSL Certificates Matter

Without HTTPS

  • Passwords visible - Anyone on your network sees login credentials
  • Session hijacking - Attackers steal cookies, impersonate you
  • MITM attacks - Traffic can be intercepted and modified
  • Browser warnings - “Not Secure” labels scare users
  • Modern features broken - Service workers, webcam access, etc. require HTTPS

With HTTPS

  • End-to-end encryption - Data scrambled between browser and server
  • Authentication - Certificate proves you’re talking to the right server
  • Trust indicators - Green padlock in browser
  • Full feature support - PWAs, geolocation, notifications work
  • Compliance - Required for many services (OAuth, payment processing)

Prerequisites

1. Domain Name

You need a domain pointing to your server. Let’s Encrypt validates domain ownership.

Options:

  • Buy domain: Namecheap, Porkbun, Cloudflare ($8-15/year)
  • Free subdomain: DuckDNS, FreeDNS (limited features)

2. Ports Open

Let’s Encrypt validates via HTTP (port 80) or HTTPS (port 443). Open these on your router:

EExxtteerrnnaallPPoorrtt84043IInntteerrnnaallPPoorrtt84043((yyoouurrsseerrvveerr))

3. DNS Pointing to Your Server

Set an A record at your domain registrar:

TNVTyaaTpmlLeeu:::e:3A@6Y0O0UoRr_PsUuBbLdIoCm_aIiPnlike"home")

Find your public IP:

curl ifconfig.me

Test DNS propagation:

dig yourdomain.com
# Should show your public IP

Method 1: Nginx Proxy Manager (Easiest)

Best for: Beginners, multiple services, GUI preference

Install

mkdir -p ~/nginx-proxy-manager
cd ~/nginx-proxy-manager
nano docker-compose.yml

Paste:

version: '3.8'
services:
  app:
    image: 'jc21/nginx-proxy-manager:latest'
    restart: unless-stopped
    ports:
      - '80:80'
      - '443:443'
      - '81:81'
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt

Start:

docker compose up -d

Configure

  1. Access admin panel: http://your-server-ip:81
  2. Default login:
  3. Change credentials immediately

Add SSL Certificate

Step 1: Add Proxy Host

  • Click “Hosts” → “Proxy Hosts” → “Add Proxy Host”
  • Domain Names: yourdomain.com
  • Scheme: http
  • Forward Hostname: localhost (or container name)
  • Forward Port: Service port (e.g., 8080 for Nextcloud)
  • Block Common Exploits

Step 2: Enable SSL

  • Click “SSL” tab
  • SSL Certificate: “Request a new SSL Certificate”
  • Force SSL
  • HTTP/2 Support
  • Email: Your email for renewal notices
  • Agree to Let’s Encrypt Terms
  • Click Save

Done! NPM requests certificate, installs it, and auto-renews every 90 days.

Add Multiple Subdomains

Repeat for each service:

  • nextcloud.yourdomain.comlocalhost:8080
  • jellyfin.yourdomain.comlocalhost:8096
  • vaultwarden.yourdomain.comlocalhost:8000

Each gets its own free certificate.

Wildcards (Advanced)

For *.yourdomain.com certificates, use DNS challenge:

  1. Get Cloudflare API token
  2. In NPM, select “DNS Challenge”
  3. Choose “Cloudflare”
  4. Paste API token
  5. Request wildcard: *.yourdomain.com

Now all subdomains share one certificate.

Method 2: Caddy (Automatic HTTPS)

Best for: Zero-config lovers, simple setups, single service

Caddy gets SSL certificates automatically with zero configuration.

Install Caddy

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
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

Configure Caddyfile

Edit /etc/caddy/Caddyfile:

y}ourdroemvaeirns.ec_opmro{xylocalhost:8080

That’s it. Caddy automatically:

  • Requests Let’s Encrypt certificate
  • Redirects HTTP → HTTPS
  • Renews certificates
  • Enables HTTP/2 and HTTP/3

Reload Caddy

sudo systemctl reload caddy

Visit https://yourdomain.com - SSL works immediately.

Multiple Services

n}j}v}eeaxlutllcryrtrlefeweovivavueneredr.rdr.sysesyeoeneo_u_._uprpyprrdrordooouooxmxrxmyaydyaioilnlmlno.oao.ccciccaoanaolml.lmhhcho{ooo{ssmsttt::{:888000890060

Each subdomain gets automatic HTTPS.

Custom Options

y}ourdr#h}#r}oeeamvCaRtaeudaeirseSXXt_z}nstrt--elo.eorCFinc_m{iorlmeopcnaiikewmrhttmmtyevioe-eeioyen{xaTn-t{undydrtOir{toea-pndrswlrnTtgoeossyimm11cppoao0maoenit0lr-snehtO_o-p"{hsStSoteiAs:coMt8unE}0rsO8iR0t"IynGoI"sNmn"aixf-fa"ge=31536000;includeSubDomains;preload"

Method 3: Certbot (Manual)

Best for: Nginx/Apache users, maximum control, custom setups

Install Certbot

sudo apt update
sudo apt install certbot python3-certbot-nginx -y

Or for Apache:

sudo apt install certbot python3-certbot-apache -y

Get Certificate (Nginx)

sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Certbot will:

  1. Validate domain ownership
  2. Generate certificate
  3. Update Nginx config
  4. Set up auto-renewal

Get Certificate (Apache)

sudo certbot --apache -d yourdomain.com -d www.yourdomain.com

Standalone (No Web Server)

If you want certificates without a web server:

sudo certbot certonly --standalone -d yourdomain.com

Certificates go to:

//eettcc//lleettsseennccrryypptt//lliivvee//yyoouurrddoommaaiinn..ccoomm//fpurlilvckheayi.np.epmem

Manual Nginx Config

If Certbot doesn’t auto-configure, edit /etc/nginx/sites-available/default:

server {
    listen 80;
    server_name yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name yourdomain.com;
    
    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    
    # Modern SSL config
    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;
    }
}

Test and reload:

sudo nginx -t
sudo systemctl reload nginx

Auto-Renewal

Certbot sets up auto-renewal via systemd timer:

sudo systemctl status certbot.timer

Test renewal:

sudo certbot renew --dry-run

If successful, certificates auto-renew before expiration.

DNS Challenge (For Local Servers)

If you can’t open ports 80/443 (CGNAT, corporate network), use DNS challenge.

With Certbot + Cloudflare

  1. Get Cloudflare API token:

    • Cloudflare dashboard → My Profile → API Tokens
    • Create token with Zone:DNS:Edit permissions
  2. Install plugin:

    sudo apt install python3-certbot-dns-cloudflare
    
  3. Create credentials file:

    sudo nano /root/.secrets/cloudflare.ini
    

    Paste:

    dns_cloudflare_api_token = YOUR_TOKEN_HERE
    

    Secure it:

    sudo chmod 600 /root/.secrets/cloudflare.ini
    
  4. Request certificate:

    sudo certbot certonly \
      --dns-cloudflare \
      --dns-cloudflare-credentials /root/.secrets/cloudflare.ini \
      -d yourdomain.com \
      -d '*.yourdomain.com'
    

Now you have SSL without exposing your server publicly!

With Caddy + Cloudflare

Install Caddy with DNS plugin:

sudo caddy add-package github.com/caddy-dns/cloudflare

Caddyfile:

y}ourdt}rolemsvaei{drnns.sec_ocpmlroo{uxdyfllaorceal{heonsvt.:C8L0O8U0DFLARE_API_TOKEN}

Set environment variable:

export CLOUDFLARE_API_TOKEN="your_token"

Troubleshooting

“Unable to validate domain”

Causes:

  • DNS not propagated yet (wait 5-10 minutes)
  • Firewall blocking port 80/443
  • Wrong IP in DNS A record

Check:

# Test port 80 is reachable
curl -I http://yourdomain.com

# Verify DNS
dig yourdomain.com

# Check firewall
sudo ufw status

“Certificate already exists”

If certificate exists but not installed:

sudo certbot certificates

Shows certificate location. Update your web server config to use it.

“Too many requests”

Let’s Encrypt rate limit: 5 failed attempts per hour, 50 certificates per domain per week.

Fix: Wait 1 hour and try again.

“Connection refused”

Your service isn’t running or is on wrong port.

Check:

sudo netstat -tulpn | grep :8080

Ensure service is running and listening on expected port.

Browser shows “Not Secure”

Mixed content - Page loads HTTPS but includes HTTP resources (images, scripts).

Fix: Update URLs to use https:// or protocol-relative //domain.com/....

Best Practices

1. Use HSTS

Force browsers to always use HTTPS:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

2. Redirect HTTP → HTTPS

Never serve content over HTTP:

server {
    listen 80;
    server_name yourdomain.com;
    return 301 https://$server_name$request_uri;
}

3. Monitor Expiration

Certificates expire in 90 days. Most tools auto-renew at 30 days.

Check status:

sudo certbot certificates

Or use monitoring:

echo | openssl s_client -servername yourdomain.com -connect yourdomain.com:443 2>/dev/null | openssl x509 -noout -dates

4. Test SSL Configuration

Check your SSL setup:

5. Backup Certificates

Certificates are in /etc/letsencrypt/. Back up this directory:

sudo tar czf letsencrypt-backup.tar.gz /etc/letsencrypt

Store backup offsite.

Comparison Table

MethodDifficultyAuto-RenewalGUIBest For
Nginx Proxy ManagerEasy✅ Yes✅ YesBeginners, multiple services
CaddyEasy✅ Yes❌ NoSimple setups, auto config
CertbotMedium✅ Yes❌ NoNginx/Apache, advanced users

Security Checklist

  • SSL certificate installed and valid
  • HTTP redirects to HTTPS
  • HSTS header enabled
  • No mixed content warnings
  • Auto-renewal configured
  • Firewall allows ports 80/443
  • DNS points to correct IP
  • Tested with SSL Labs (A+ rating)
  • Certificates backed up
  • Monitoring for expiration set up

What About Internal-Only Services?

If you only access services from your local network, you have options:

1. Self-Signed Certificates

Free but browsers show security warnings.

Generate:

openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout /etc/ssl/private/selfsigned.key \
  -out /etc/ssl/certs/selfsigned.crt

Use in Nginx:

ssl_certificate /etc/ssl/certs/selfsigned.crt;
ssl_certificate_key /etc/ssl/private/selfsigned.key;

You’ll need to add the certificate to your browser’s trusted store.

2. mkcert (Local CA)

Create locally-trusted certificates:

sudo apt install libnss3-tools
wget https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64
sudo mv mkcert-v1.4.4-linux-amd64 /usr/local/bin/mkcert
sudo chmod +x /usr/local/bin/mkcert
mkcert -install

Generate certificate:

mkcert yourdomain.local localhost 192.168.1.100

Creates yourdomain.local+2.pem and yourdomain.local+2-key.pem.

Use in your web server - no browser warnings!

3. Tailscale HTTPS

If using Tailscale VPN:

tailscale cert yourhostname.your-tailnet.ts.net

Gets valid HTTPS certificate for your Tailscale hostname.

Final Thoughts

There’s no reason to skip HTTPS in 2025. Free certificates, automatic renewal, and easy tools make it trivial.

Start with Nginx Proxy Manager if you’re unsure. It’s the easiest path to HTTPS for multiple services.


Next Steps:

Questions? Join our Discord community or drop a comment below.