GUIDE, WITHOUT THE GUESSWORK

SSL Certificates on VPS: Complete Let's Encrypt Guide

Set up free SSL certificates on your VPS using Let's Encrypt with Certbot, Traefik, or Caddy. Includes auto-renewal and wildcard certificates.

SSL Certificates on VPS: Complete Let's Encrypt Guide

Get free SSL certificates for your VPS using Let's Encrypt. This guide covers multiple methods: Certbot with Nginx/Apache, Traefik, and Caddy.

Overview

Let's Encrypt provides free, automated SSL certificates. You have several options:

MethodBest ForDifficulty
Certbot + NginxTraditional setupsEasy
Certbot + ApachePHP/WordPressEasy
TraefikDocker containersMedium
CaddyAutomatic everythingEasiest
Certbot standaloneNo web serverEasy

Before you start

Verify your domain points to your VPS:

dig +short yourdomain.com

This should return your VPS IP address.

Ensure ports 80 and 443 are open:

sudo ufw allow 80
sudo ufw allow 443
sudo ufw status

Method 1: Certbot with Nginx

Install Certbot

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

Get the certificate

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

Certbot will:

  1. Verify you control the domain
  2. Obtain the certificate
  3. Configure Nginx to use HTTPS
  4. Set up auto-renewal

Follow the prompts:

Verify the configuration

sudo nginx -t
sudo systemctl reload nginx

Visit https://yourdomain.com - you should see a padlock.

Verify auto-renewal

sudo certbot renew --dry-run

Certbot installs a systemd timer that runs twice daily:

sudo systemctl status certbot.timer

Method 2: Certbot with Apache

Install Certbot

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

Get the certificate

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

Verify

sudo apachectl configtest
sudo systemctl reload apache2

Method 3: Certbot standalone (no web server)

Use this when you do not have a web server running or want to manage certificates separately.

Stop any service on port 80

sudo systemctl stop nginx  # or apache2

Get the certificate

sudo certbot certonly --standalone -d yourdomain.com -d www.yourdomain.com

Certificates are saved to:

Restart your service

sudo systemctl start nginx

Configure renewal hooks

Since standalone needs port 80 free, add hooks:

sudo nano /etc/letsencrypt/renewal/yourdomain.com.conf

Add at the end:

[renewalparams]
pre_hook = systemctl stop nginx
post_hook = systemctl start nginx

Method 4: Traefik (Docker)

Traefik handles SSL automatically for Docker containers.

Create docker-compose.yml

services:
  traefik:
    image: traefik:v2.11
    command:
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --entrypoints.web.http.redirections.entrypoint.to=websecure
      - --certificatesresolvers.letsencrypt.acme.tlschallenge=true
      - [email protected]
      - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
    ports:
      - 80:80
      - 443:443
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - letsencrypt:/letsencrypt
    restart: unless-stopped

  app:
    image: your-app:latest
    labels:
      - traefik.enable=true
      - traefik.http.routers.app.rule=Host(`yourdomain.com`)
      - traefik.http.routers.app.tls=true
      - traefik.http.routers.app.tls.certresolver=letsencrypt
      - traefik.http.services.app.loadbalancer.server.port=3000

volumes:
  letsencrypt:

Start the stack

docker compose up -d

Traefik automatically obtains and renews certificates for any container with the appropriate labels.

Method 5: Caddy (automatic SSL)

Caddy handles SSL 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 -y

Configure Caddy

sudo nano /etc/caddy/Caddyfile

For a reverse proxy:

yourdomain.com {
    reverse_proxy localhost:3000
}

anotherdomain.com {
    reverse_proxy localhost:3001
}

For a static site:

yourdomain.com {
    root * /var/www/html
    file_server
}

Start Caddy

sudo systemctl restart caddy

That is it. Caddy automatically obtains and renews certificates.

Wildcard certificates

Wildcard certificates cover all subdomains: *.yourdomain.com

Requirements

Wildcard certificates require DNS-01 challenge (not HTTP). You need:

Example with Cloudflare

Install the plugin:

sudo apt install python3-certbot-dns-cloudflare -y

Create credentials file:

sudo mkdir -p /etc/letsencrypt
sudo nano /etc/letsencrypt/cloudflare.ini
dns_cloudflare_api_token = your-api-token

Secure the file:

sudo chmod 600 /etc/letsencrypt/cloudflare.ini

Get the wildcard certificate:

sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d yourdomain.com \
  -d "*.yourdomain.com"

DNS plugins for other providers

ProviderPlugin
Cloudflarepython3-certbot-dns-cloudflare
DigitalOceanpython3-certbot-dns-digitalocean
Route53python3-certbot-dns-route53
Google Cloudpython3-certbot-dns-google

Certificate locations

Certbot stores certificates in:

/etc/letsencrypt/live/yourdomain.com/
├── cert.pem       # Your certificate
├── chain.pem      # Intermediate certificates
├── fullchain.pem  # cert.pem + chain.pem (use this)
├── privkey.pem    # Private key

Always use fullchain.pem for the certificate and privkey.pem for the key.

Manual renewal

Force renewal of all certificates:

sudo certbot renew --force-renewal

Renew a specific certificate:

sudo certbot certonly --force-renewal -d yourdomain.com

Check certificate expiration

sudo certbot certificates

Or check a specific domain:

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

Rate limits

Let's Encrypt has rate limits:

LimitValue
Certificates per domain50 per week
Duplicate certificates5 per week
Failed validations5 per hour

For testing, use the staging environment:

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

Remove --staging when ready for production.

Troubleshooting

Challenge failed - connection refused

Port 80 must be open and accessible:

sudo ufw allow 80
sudo ss -tlnp | grep :80

Test from outside:

curl -I http://yourdomain.com

DNS problem - NXDOMAIN

Your DNS is not pointing to your VPS:

dig +short yourdomain.com

Wait for DNS propagation (up to 48 hours, usually minutes).

Too many certificates already issued

You hit rate limits. Wait a week or use a different subdomain for testing.

Certificate not trusted

You are using the staging certificate. Re-run Certbot without --staging.

Auto-renewal not working

Check the timer:

sudo systemctl status certbot.timer
journalctl -u certbot

Test renewal:

sudo certbot renew --dry-run

Permission denied reading certificates

Ensure proper permissions:

sudo chmod 755 /etc/letsencrypt/live
sudo chmod 755 /etc/letsencrypt/archive

For non-root services, add them to the ssl-cert group:

sudo usermod -aG ssl-cert www-data
sudo chgrp -R ssl-cert /etc/letsencrypt/live
sudo chgrp -R ssl-cert /etc/letsencrypt/archive

Nginx SSL best practices

After getting your certificate, optimize your Nginx SSL config:

sudo nano /etc/nginx/sites-available/yourdomain.com
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 configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 1.1.1.1 8.8.8.8 valid=300s;

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

    # ... rest of your config
}

Test your SSL configuration at SSL Labs.

Where to go next

Tutorials:

Comparisons:

ServerCompass:

From across the StoicSoft network

Hand-curated reads on the same topic from sister sites in the StoicSoft family.