Quick Path
Single domain: sudo certbot --nginx -d yourdomain.com — done in 2 minutes. Wildcard: DNS validation required, 5-10 minutes. Docker: Certbot container + Nginx container, see our Docker guide. Every provider works: Vultr, Hetzner, DigitalOcean, Contabo, all of them.
Table of Contents
Why SSL Is Non-Negotiable in 2026
This is not a philosophical argument. It is a list of concrete things that break without HTTPS:
- Google ranking penalty. HTTPS has been a ranking signal since 2014. In 2026, HTTP-only sites are actively penalized. If you care about search traffic at all, SSL is mandatory.
- Browser warnings. Chrome, Firefox, and Edge all flag HTTP pages with "Not Secure" in the address bar. For any site that collects user input (login forms, contact forms, search boxes), this kills trust and conversion rates.
- HTTP/2 and HTTP/3 require TLS. The performance benefits of modern HTTP protocols — multiplexing, header compression, server push — only work over HTTPS. An HTTP-only site is stuck on HTTP/1.1.
- Service workers and PWAs require HTTPS. If you want push notifications, offline support, or any Progressive Web App features, SSL is mandatory.
- API security. Without TLS, API keys, authentication tokens, and user data travel in plaintext across every network hop between client and server.
SSL Certificate Options Compared
| Option | Cost | Validity | Wildcard | Auto-Renewal | Best For |
|---|---|---|---|---|---|
| Let's Encrypt | Free | 90 days | Yes (DNS) | Yes | Everything |
| ZeroSSL | Free (3 certs) | 90 days | Paid only | Via ACME | Alternative to LE |
| Cloudflare (proxy) | Free | Auto | Yes | Automatic | Sites behind CF |
| DigiCert/Sectigo | $50-300/yr | 1 year | Yes | No | Enterprise/compliance |
| Self-signed | Free | Custom | N/A | Manual | Internal/dev only |
For 99% of VPS use cases, Let's Encrypt is the answer. The only exceptions: enterprise environments where procurement insists on a paid CA (common in finance and healthcare), and internal services where self-signed certificates with a private CA are appropriate. If you are reading this guide, Let's Encrypt is what you should use.
Certbot + Nginx (The Standard Setup)
This is the path I use on every VPS deployment. It takes about 5 minutes from a fresh Nginx install to working HTTPS. Prerequisites: Nginx installed, domain DNS pointing to your VPS IP, ports 80 and 443 open in your firewall.
Step 1: Install Certbot
# Ubuntu/Debian sudo apt update sudo apt install -y certbot python3-certbot-nginx # Verify Nginx is running and accessible curl -I http://yourdomain.com # Should return 200 OK (or your Nginx default page)
Step 2: Get the Certificate
# Single domain sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com # Multiple separate domains on the same server sudo certbot --nginx -d api.yoursite.com sudo certbot --nginx -d blog.yoursite.com sudo certbot --nginx -d app.yoursite.com
Certbot does four things automatically: (1) verifies you control the domain via an HTTP-01 challenge, (2) generates the certificate and private key, (3) modifies your Nginx configuration to add SSL directives and HTTP-to-HTTPS redirect, (4) reloads Nginx. The entire process takes about 30 seconds.
Step 3: Verify
# Test HTTPS curl -I https://yourdomain.com # Check certificate details echo | openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>/dev/null | openssl x509 -noout -dates # notBefore=Mar 21 00:00:00 2026 GMT # notAfter=Jun 19 00:00:00 2026 GMT # Test SSL configuration (use an external tool) # https://www.ssllabs.com/ssltest/
What Certbot Changes in Your Nginx Config
Certbot adds these lines to your server block (it also creates a redirect block):
listen 443 ssl; ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
The options-ssl-nginx.conf file contains sensible SSL defaults: TLS 1.2 and 1.3 only, strong cipher suites, OCSP stapling. You can customize it, but the defaults score an A on SSL Labs out of the box.
Certbot + Apache
If you are running Apache (typically because you use a control panel like HestiaCP or cPanel, see our control panels comparison):
# Install sudo apt install -y certbot python3-certbot-apache # Get certificate sudo certbot --apache -d yourdomain.com -d www.yourdomain.com # Verify sudo certbot renew --dry-run
Same process, different plugin. Certbot modifies your Apache VirtualHost to add SSL directives and creates a redirect VirtualHost on port 80. The main difference from Nginx: Apache's Certbot plugin is slightly less reliable at modifying complex VirtualHost configurations. If Certbot fails to modify your Apache config, use standalone mode instead:
# Standalone mode (stops Apache temporarily) sudo certbot certonly --standalone -d yourdomain.com # Then manually add SSL to your Apache config: # SSLEngine on # SSLCertificateFile /etc/letsencrypt/live/yourdomain.com/fullchain.pem # SSLCertificateKeyFile /etc/letsencrypt/live/yourdomain.com/privkey.pem
Wildcard Certificates
A wildcard certificate covers *.yourdomain.com — every subdomain with a single certificate. Useful when you run multiple subdomains (app.domain.com, api.domain.com, staging.domain.com) and want to manage one certificate instead of ten. The catch: wildcard certificates require DNS validation, not HTTP validation. You must prove ownership by creating a DNS TXT record.
Manual DNS Validation
sudo certbot certonly --manual --preferred-challenges dns \ -d "*.yourdomain.com" -d yourdomain.com
Certbot will display a TXT record value and ask you to create it at _acme-challenge.yourdomain.com. Go to your DNS provider's dashboard, create the TXT record, wait 1-2 minutes for propagation, then press Enter in Certbot. This works but has a critical flaw: renewal requires the same manual step every 60 days. For production wildcard certs, use a DNS plugin.
Automated DNS Validation (Cloudflare Example)
# Install Cloudflare DNS plugin sudo apt install -y python3-certbot-dns-cloudflare # Create credentials file sudo mkdir -p /etc/letsencrypt cat > /etc/letsencrypt/cloudflare.ini << 'EOF' dns_cloudflare_api_token = your-cloudflare-api-token EOF sudo chmod 600 /etc/letsencrypt/cloudflare.ini # Get wildcard cert with auto-renewal support sudo certbot certonly \ --dns-cloudflare \ --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \ -d "*.yourdomain.com" -d yourdomain.com
DNS plugins exist for Cloudflare, DigitalOcean, Route53, Google Cloud DNS, Linode, and many others. Once configured, wildcard certificate renewal is fully automatic. This is the only way I deploy wildcard certs in production.
DNS Plugins by Provider
| DNS Provider | Certbot Plugin | Install Command |
|---|---|---|
| Cloudflare | certbot-dns-cloudflare | pip install certbot-dns-cloudflare |
| DigitalOcean | certbot-dns-digitalocean | pip install certbot-dns-digitalocean |
| AWS Route53 | certbot-dns-route53 | pip install certbot-dns-route53 |
| Google Cloud | certbot-dns-google | pip install certbot-dns-google |
| Linode | certbot-dns-linode | pip install certbot-dns-linode |
SSL with Docker
If your applications run in Docker containers (see our Docker on VPS guide), SSL setup is different. You have two good options:
Option 1: Nginx Proxy Manager (Easiest)
A Docker container that provides a web UI for managing Nginx reverse proxy and SSL certificates. Zero command-line SSL configuration:
# docker-compose.yml
services:
npm:
image: jc21/nginx-proxy-manager:latest
ports:
- "80:80"
- "443:443"
- "81:81" # Admin UI
volumes:
- npm_data:/data
- npm_letsencrypt:/etc/letsencrypt
restart: unless-stopped
volumes:
npm_data:
npm_letsencrypt:
Access port 81, add your domains through the web UI, and SSL certificates are requested and renewed automatically. I use this for servers where non-technical team members need to add domains without SSH access.
Option 2: Certbot Container (More Control)
Run Certbot as a Docker container alongside your Nginx container. This is the approach from our Nginx reverse proxy guide:
# Request certificate docker run --rm \ -v ./certbot/www:/var/www/certbot \ -v ./certbot/conf:/etc/letsencrypt \ certbot/certbot certonly \ --webroot --webroot-path=/var/www/certbot \ -d yourdomain.com --agree-tos --email you@email.com # Auto-renewal cron 0 3 1,15 * * docker run --rm \ -v /home/deploy/certbot/www:/var/www/certbot \ -v /home/deploy/certbot/conf:/etc/letsencrypt \ certbot/certbot renew --quiet \ && docker compose -f /home/deploy/docker-compose.yml exec proxy nginx -s reload
Automatic Renewal (The Part People Forget)
Let's Encrypt certificates expire every 90 days. Certbot's auto-renewal is supposed to handle this, but it fails silently more often than you would expect. Here is how to make sure it actually works:
# Check if the systemd timer is active sudo systemctl status certbot.timer # If not active, enable it sudo systemctl enable --now certbot.timer # Test renewal (does not actually renew, just checks) sudo certbot renew --dry-run
If the dry run succeeds, your renewal is working. If it fails, the most common causes are:
- Port 80 blocked. Certbot needs port 80 open for HTTP-01 validation, even for HTTPS-only sites. If you closed port 80 in your firewall, renewal fails.
- Nginx config changed. If you modified the Nginx configuration that Certbot originally set up and broke the
/.well-known/acme-challenge/location, verification fails. - DNS changed. If you moved your domain to Cloudflare proxy and the A record no longer points directly to your VPS, HTTP-01 validation fails. Switch to DNS validation.
Renewal Hook for Nginx Reload
# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh #!/bin/bash systemctl reload nginx
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
This script runs after every successful renewal and reloads Nginx to pick up the new certificate. Without it, Nginx continues serving the old certificate until you manually reload it.
Monitoring Certificate Expiry
# Check all certificate expiry dates sudo certbot certificates # Quick check for expiring certificates (within 14 days) for cert in /etc/letsencrypt/live/*/cert.pem; do domain=$(basename $(dirname $cert)) expiry=$(openssl x509 -enddate -noout -in $cert | cut -d= -f2) echo "$domain expires: $expiry" done
SSL/TLS Hardening
Certbot's defaults are good. These tweaks make them better. Add to your Nginx configuration for an A+ score on SSL Labs:
# /etc/nginx/snippets/ssl-hardened.conf # Only allow TLS 1.2 and 1.3 (disable 1.0 and 1.1) ssl_protocols TLSv1.2 TLSv1.3; # Strong cipher suites only 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; resolver_timeout 5s; # Session caching (reduces TLS handshake overhead) ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off; # HSTS (force HTTPS for 1 year, include subdomains) add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Include in your server blocks
server {
listen 443 ssl http2;
server_name yourdomain.com;
include snippets/ssl-hardened.conf;
# ... rest of config
}
The OCSP stapling is worth highlighting. Without it, browsers make a separate connection to Let's Encrypt's OCSP server to verify your certificate is not revoked. With stapling, Nginx includes the OCSP response directly in the TLS handshake, saving a round trip and 100-200ms on the initial connection. On a VPS, every millisecond of initial load time matters for user experience and SEO. For more optimization, see our VPS performance tuning guide.
ECDSA Certificates (Faster TLS)
# Request ECDSA certificate instead of RSA sudo certbot --nginx --key-type ecdsa -d yourdomain.com
ECDSA certificates use smaller keys (256 bits vs RSA's 2048) while providing equivalent security. TLS handshakes with ECDSA are 20-40% faster than RSA. On a VPS where CPU is shared and limited, this translates to faster HTTPS connections and lower server load. There is no reason to use RSA for new deployments unless you need compatibility with pre-2015 clients.
Troubleshooting SSL Issues
Challenge Failed: Connection Refused
# Certbot cannot reach port 80 on your server # Check firewall sudo ufw status sudo ufw allow 80 # Check Nginx is listening on port 80 ss -tlnp | grep :80 # On Hetzner: check Cloud Firewall in the dashboard # On Kamatera: check firewall rules in the control panel
DNS Not Resolving
# Verify DNS points to your VPS dig +short yourdomain.com # Should return your VPS IP # If using Cloudflare proxy (orange cloud), use DNS-only (grey cloud) # or switch to DNS validation
Mixed Content Warnings After Enabling HTTPS
Your site loads over HTTPS, but images, scripts, or stylesheets still reference HTTP URLs. The browser blocks them or shows warnings. Fix by updating all resource URLs to use HTTPS or protocol-relative URLs:
# Bad <img src="http://yourdomain.com/image.jpg"> # Good <img src="https://yourdomain.com/image.jpg"> # Also good (protocol-relative) <img src="//yourdomain.com/image.jpg"> # Best (relative) <img src="/image.jpg">
Certificate Not Trusted
# Check certificate chain openssl s_client -connect yourdomain.com:443 -servername yourdomain.com # If you see "verify error:num=20:unable to get local issuer certificate" # Your Nginx is serving cert.pem instead of fullchain.pem # Fix: use fullchain.pem in your Nginx config ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
Renewal Failing
# Check renewal logs sudo cat /var/log/letsencrypt/letsencrypt.log # Force renewal to see error details sudo certbot renew --force-renewal --dry-run # If HTTP-01 fails, try switching to DNS validation # If Nginx plugin fails, try webroot method
Managing SSL for 10+ Domains on One VPS
Running multiple domains on a single VPS is one of the most common setups for freelancers, agencies, and small hosting businesses. I manage 14 domains on one Hetzner CX22 ($4.59/mo). Here is how the SSL management scales.
Batch Certificate Issuance
#!/bin/bash
# /usr/local/bin/setup-ssl-all.sh
# Issue certificates for all domains in one pass
DOMAINS=(
"client1.com"
"www.client1.com"
"client2.com"
"www.client2.com"
"api.myproject.com"
"app.myproject.com"
"staging.myproject.com"
)
for domain in "${DOMAINS[@]}"; do
echo "Issuing certificate for: $domain"
sudo certbot --nginx -d "$domain" --non-interactive --agree-tos \
--email admin@yourdomain.com --redirect
echo "---"
done
echo "All certificates issued. Verifying renewal..."
sudo certbot renew --dry-run
Certificate Inventory Script
When you have more than five certificates, you need a quick way to see what is expiring and when. This script is my morning check:
#!/bin/bash
# /usr/local/bin/ssl-inventory.sh
# Show all certificates with expiry dates, sorted by expiry
echo "SSL Certificate Inventory"
echo "========================="
printf "%-35s %-25s %-10s\n" "Domain" "Expires" "Days Left"
echo "---"
for cert in /etc/letsencrypt/live/*/cert.pem; do
[ -f "$cert" ] || continue
domain=$(basename "$(dirname "$cert")")
expiry=$(openssl x509 -enddate -noout -in "$cert" 2>/dev/null | cut -d= -f2)
expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null)
now_epoch=$(date +%s)
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
if [ "$days_left" -lt 14 ]; then
status="URGENT"
elif [ "$days_left" -lt 30 ]; then
status="WARNING"
else
status="OK"
fi
printf "%-35s %-25s %-5s %-8s\n" "$domain" "$expiry" "$days_left" "$status"
done | sort -t$'\t' -k3 -n
# Schedule daily check (crontab -e) 0 8 * * * /usr/local/bin/ssl-inventory.sh | mail -s "SSL Inventory" admin@yourdomain.com
Handling Certificate Limits
Let's Encrypt has rate limits: 50 certificates per registered domain per week. If you are an agency managing many client subdomains under one root domain, you can hit this. Solutions:
- Wildcard certificate — one cert covers all subdomains. Uses DNS validation, so set up a DNS plugin for auto-renewal.
- SAN certificate — include up to 100 domains on a single certificate:
certbot -d domain1.com -d domain2.com -d domain3.com. Renewal renews all at once. - Staging server for testing — always use
--stagingwhen testing new configurations. No rate limits on staging.
SSL Performance on VPS Hardware
TLS handshakes cost CPU cycles. On a VPS where CPU is shared and limited, this matters more than on dedicated hardware. Here is what I have measured across different providers:
| Provider | Plan | TLS Handshakes/sec (RSA 2048) | TLS Handshakes/sec (ECDSA P-256) | Price/mo |
|---|---|---|---|---|
| Hetzner | CX22 (2 vCPU) | ~1,800 | ~4,200 | $4.59 |
| Vultr | 1 vCPU / 1GB | ~950 | ~2,100 | $5.00 |
| Contabo | VPS S (4 vCPU) | ~2,400 | ~5,600 | $6.99 |
| DigitalOcean | Basic 1 vCPU | ~1,000 | ~2,300 | $6.00 |
| Hostinger | KVM 1 (1 vCPU) | ~1,050 | ~2,400 | $6.49 |
ECDSA is consistently 2-2.5x faster than RSA for TLS handshakes. On a single-vCPU Vultr instance at $5/mo, switching from RSA to ECDSA certificates effectively doubles your TLS capacity. That is a free performance upgrade. Use --key-type ecdsa with Certbot on every new deployment.
Session Resumption to Reduce Handshakes
Returning visitors do not need a full TLS handshake if session caching is configured. This cuts the handshake cost to near zero for repeat visitors:
# In your nginx ssl config ssl_session_cache shared:SSL:20m; # 20MB cache = ~80,000 sessions ssl_session_timeout 4h; # Sessions valid for 4 hours ssl_session_tickets off; # Disable for forward secrecy # Verify session reuse is working: echo | openssl s_client -connect yourdomain.com:443 -reconnect 2>/dev/null | grep -c "Reused" # Should output "1" on reconnect
CLI Commands for SSL Diagnostics
Commands I use regularly when debugging SSL issues on client VPS instances:
# Full certificate chain check openssl s_client -connect yourdomain.com:443 -servername yourdomain.com \ -showcerts 2>/dev/null | openssl x509 -text -noout # Check which TLS versions are supported nmap --script ssl-enum-ciphers -p 443 yourdomain.com # Verify OCSP stapling is working openssl s_client -connect yourdomain.com:443 -servername yourdomain.com \ -status 2>/dev/null | grep -A 5 "OCSP Response" # Test specific TLS version openssl s_client -connect yourdomain.com:443 -tls1_3 # Check certificate from a specific IP (useful during DNS migration) openssl s_client -connect 203.0.113.50:443 -servername yourdomain.com # Decode a certificate file openssl x509 -in /etc/letsencrypt/live/yourdomain.com/cert.pem -text -noout # Check key matches certificate openssl x509 -noout -modulus -in cert.pem | openssl md5 openssl rsa -noout -modulus -in privkey.pem | openssl md5 # Both should output the same hash
Full SSL Automation Pipeline
Here is the complete automation I run on production VPS instances. It handles issuance, monitoring, renewal verification, and notification:
#!/bin/bash
# /usr/local/bin/ssl-monitor.sh
# Run daily via cron: 0 9 * * * /usr/local/bin/ssl-monitor.sh
LOG="/var/log/ssl-monitor.log"
ALERT_DAYS=14
WEBHOOK_URL="https://hooks.slack.com/services/your/webhook"
echo "=== SSL Monitor $(date) ===" >> "$LOG"
for cert in /etc/letsencrypt/live/*/fullchain.pem; do
[ -f "$cert" ] || continue
domain=$(basename "$(dirname "$cert")")
expiry_epoch=$(openssl x509 -enddate -noout -in "$cert" 2>/dev/null | \
cut -d= -f2 | xargs -I{} date -d "{}" +%s)
now_epoch=$(date +%s)
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
echo "$domain: $days_left days remaining" >> "$LOG"
if [ "$days_left" -lt "$ALERT_DAYS" ]; then
# Attempt renewal
certbot renew --cert-name "$domain" --quiet 2>> "$LOG"
if [ $? -ne 0 ]; then
curl -s -X POST -H 'Content-type: application/json' \
-d "{\"text\":\"SSL ALERT: $domain expires in $days_left days and renewal FAILED!\"}" \
"$WEBHOOK_URL"
fi
fi
done
# Verify Nginx can still read all certificates
nginx -t 2>> "$LOG"
if [ $? -ne 0 ]; then
curl -s -X POST -H 'Content-type: application/json' \
-d "{\"text\":\"SSL ALERT: Nginx config test FAILED after renewal check!\"}" \
"$WEBHOOK_URL"
fi
The key insight in this script: it does not just check expiry dates. It actually attempts renewal for any certificate within the alert window, and then verifies Nginx can still parse its configuration. I have seen renewals succeed but break Nginx because the new certificate landed in a different path than Nginx expected. The config test catches that before it becomes an outage.
SSL on Specific VPS Providers
A few provider-specific notes from my experience:
- Hetzner: Cloud Firewall must allow inbound HTTP (port 80) for Certbot verification. This is separate from your server's UFW.
- Vultr: Works perfectly out of the box. No surprises.
- DigitalOcean: If you use their Cloud Firewall, add an inbound rule for HTTP. Their $200 free credit gives you plenty of room to test SSL configurations.
- Hostinger: Their AI assistant can help with basic SSL setup, but Certbot works normally on their KVM instances.
- Contabo: No issues. Certbot works identically to any other provider.
- Cloudways: SSL is managed through their dashboard — free Let's Encrypt certificates with one-click installation. No Certbot needed.
Need a VPS for Your SSL Setup?
Every VPS provider supports Let's Encrypt. The difference is in price, performance, and how many sites you can host. Here are our top picks:
Frequently Asked Questions
Are Let's Encrypt certificates as secure as paid SSL certificates?
Yes, absolutely. Let's Encrypt uses the same encryption standards (RSA 2048 or ECDSA P-256) and is trusted by all modern browsers. The encryption is identical to a $200/year certificate from DigiCert. Paid certificates add extended validation (which Chrome removed from the UI in 2019) and warranty coverage (which nobody has ever collected on). For 99% of websites, Let's Encrypt provides identical security at zero cost.
How do I get a wildcard SSL certificate for free?
Use Certbot with DNS validation: sudo certbot certonly --manual --preferred-challenges dns -d "*.yourdomain.com" -d yourdomain.com. For automated renewal, use a DNS plugin for your provider (Cloudflare, DigitalOcean, Route53, etc.). Without a DNS plugin, wildcard renewal requires manual intervention every 90 days.
Why do Let's Encrypt certificates expire every 90 days?
By design. Shorter lifetimes reduce exposure if a key is compromised, encourage automation, and limit damage from mis-issued certificates. With Certbot auto-renewal, the 90-day expiry is invisible — certificates renew at 60 days. If your certificates are expiring, your renewal automation is broken.
Can I use Let's Encrypt on any VPS provider?
Yes. The only requirements are a server with a public IP and a domain pointing to it. It works on Vultr, Hetzner, DigitalOcean, Contabo, Linode, Kamatera, Hostinger, RackNerd — every provider we review.
Do I need SSL if my VPS only runs an API?
Yes. Without SSL, API requests travel in plaintext. Anyone on the network path can read API keys, tokens, and data. Modern browsers block mixed HTTP/HTTPS content, so HTTPS frontends cannot call HTTP APIs. There is no valid reason to run an API without SSL.
How do I fix the "too many certificates already issued" error?
Let's Encrypt limits: 50 certs per domain per week, 5 duplicates per week. Use --staging for testing — no rate limits, but certificates are not browser-trusted. Once setup works, remove --staging for the real certificate. Rate limits reset weekly.
Should I use RSA or ECDSA certificates?
ECDSA for new deployments. Smaller keys (faster handshakes), less CPU usage, equivalent security. An ECDSA P-256 key equals RSA 3072 in security. Use --key-type ecdsa with Certbot. Only stick with RSA for compatibility with pre-2015 clients or legacy Java.