Tutorial

Free SSL Certificates with Let's Encrypt on a VPS

Security & Monitoring8 min read4 steps

HTTPS is no longer optional: browsers, SEO and customers all demand it. On a VPS you control end to end, Let's Encrypt issues recognized certificates that are free and renew themselves. This guide covers step-by-step certbot installation with Nginx, wildcard certificates via Cloudflare DNS challenge, systemd timer renewal verification, expiry monitoring and the most common errors — ACME rate limits, blocked port 80, DNS propagation.

Contents· Why manage Let's Encrypt yourself on a VPS1/10
  1. 01Why manage Let's Encrypt yourself on a VPS
  2. 02What you gain by self-hosting the TLS chain
  3. 03Prerequisites before you start
  4. 04Install certbot and obtain a Nginx certificate
  5. 05Wildcard certificate with Cloudflare DNS challenge
  6. 06Nginx TLS hardening: TLS 1.2/1.3, HSTS and cipher suites
  7. 07Monitoring certificate expiry
  8. 08Troubleshooting: rate limits, blocked port 80, DNS propagation
  9. 09Comparison: certbot vs acme.sh vs Caddy automatic HTTPS
  10. 10Test before production

Why manage Let's Encrypt yourself on a VPS

Let's Encrypt is a free certificate authority that issues TLS certificates validated by an automated challenge (HTTP-01 or DNS-01). On shared hosting, you are stuck with the provider's TLS config: imposed protocol versions, no OCSP stapling, opaque renewal. On a VPS, you own the certbot (or acme.sh), the reverse proxy and the TLS termination. You decide on the protocols (TLS 1.2/1.3 only), the cipher suites, HSTS and OCSP stapling. You can also issue a wildcard certificate (*.your-domain.com) via the DNS-01 challenge, cover several domains in a single certificate (SAN), and wire renewal to your own alerts. In short: total control over transport security, a prerequisite for an A+ score on SSL Labs and maximum browser-side trust.

What you gain by self-hosting the TLS chain

  • 100% free certificates, valid for 90 days and renewed automatically without intervention.
  • Wildcard *.your-domain.com possible via the DNS-01 challenge: a single certificate for all your subdomains.
  • Full control of the config: TLS 1.3, modern cipher suites, HSTS and OCSP stapling.
  • Multi-domain SAN certificates (your-domain.com, www.your-domain.com, api.your-domain.com) on a single IP.
  • Scriptable and observable renewal: reload hooks, Slack/email alerts before expiry.
  • No dependency on a third-party panel: the same recipe works across all your VPSes and environments.

Prerequisites before you start

TLS termination consumes very few resources: a VPS with 1 vCPU / 512 MB to 1 GB of RAM is more than enough to serve HTTPS for one or more sites.

On the network side: your domain name must point to the VPS's public IP (a propagated A/AAAA record). Check propagation before running certbot: dig +short your-domain.com must return your VPS's IP. A challenge that fails due to incomplete DNS propagation counts as an attempt and brings you closer to the ACME rate limits.

Ports 80 and 443 must be open in the firewall — port 80 is essential for the HTTP-01 challenge (certbot places a temporary challenge file there):

ufw allow 80/tcp
ufw allow 443/tcp
ufw reload

For a wildcard certificate (*.your-domain.com), the HTTP-01 challenge is not sufficient: you will need an API token from your DNS provider (Cloudflare, OVH…) to automate the DNS-01 challenge.

Install certbot and obtain a Nginx certificate

  1. Install certbot and the Nginx plugin

    On Debian/Ubuntu:

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

    The python3-certbot-nginx plugin reads your Nginx config, injects the TLS configuration and replaces the listen 80 directive with an 443 ssl block. If you prefer to manage Nginx manually, use --standalone or --webroot instead of --nginx.

  2. Point the domain and open the ports

    Create an A record your-domain.com (and AAAA if IPv6) pointing to the VPS's IP, then allow web traffic:

    ufw allow 80,443/tcp

    Confirm resolution with dig +short your-domain.com before going further — a challenge always fails if DNS is not yet pointing.

  3. Issue the certificate with the Nginx plugin

    The most direct method — certbot modifies your Nginx vhost and manages renewal:

    certbot --nginx -d your-domain.com -d www.your-domain.com

    Certbot asks for an email address (expiry alerts) and prompts you to accept the ToS. It issues the certificate, modifies /etc/nginx/sites-available/<vhost>, adds an HTTP→HTTPS redirect and reloads Nginx without downtime.

    Files are placed in /etc/letsencrypt/live/your-domain.com/: fullchain.pem (certificate + chain) and privkey.pem (private key).

  4. Verify automatic renewal

    Certbot installs a systemd timer that attempts renewal twice a day:

    systemctl status certbot.timer

    To simulate a renewal without issuing a new certificate:

    certbot renew --dry-run

    If the simulation succeeds, your renewal chain is operational. Make sure Nginx reloads after an actual renewal — certbot automatically adds a --deploy-hook "systemctl reload nginx" hook in /etc/letsencrypt/renewal/<domain>.conf when using the Nginx plugin.

Wildcard certificate with Cloudflare DNS challenge

A *.your-domain.com certificate covers all your subdomains with a single file. It is mandatory if you create subdomains dynamically or if you don't want to issue one certificate per subdomain.

The HTTP-01 challenge cannot validate a wildcard — you need the DNS-01 challenge, which requires certbot to temporarily create a TXT record _acme-challenge.your-domain.com.

With Cloudflare (API token):

apt install -y python3-certbot-dns-cloudflare

Create a Cloudflare credentials file (permissions: 600):

mkdir -p /etc/letsencrypt/cloudflare
chmod 700 /etc/letsencrypt/cloudflare
cat > /etc/letsencrypt/cloudflare/credentials.ini << 'EOF'
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN
EOF
chmod 600 /etc/letsencrypt/cloudflare/credentials.ini

Issue the wildcard certificate:

certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \
  -d your-domain.com \
  -d '*.your-domain.com'

Certbot creates the TXT record, waits for propagation (10 seconds by default — increase with --dns-cloudflare-propagation-seconds 30), obtains the certificate, then deletes the TXT record.

Alternative with acme.sh:

acme.sh --issue --dns dns_cf -d your-domain.com -d '*.your-domain.com'

acme.sh uses the CF_Token environment variable or CF_Key + CF_Email depending on your Cloudflare authentication method.

Nginx TLS hardening: TLS 1.2/1.3, HSTS and cipher suites

The certbot plugin configures a functional HTTPS, but the default configuration is not optimal for an A+ score on SSL Labs. Here are the directives to add to your server block (or in an included file like /etc/nginx/snippets/ssl-params.conf):

# Protocols: TLS 1.2 minimum, TLS 1.3 recommended
ssl_protocols TLSv1.2 TLSv1.3;

# Modern cipher suites (Mozilla Intermediate)
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;

# TLS session
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;

# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;

# HSTS (6 months, subdomains included, preload)
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload" always;

# Additional security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;

Redirect all of port 80 to 443 in a separate server block:

server {
    listen 80;
    server_name your-domain.com www.your-domain.com;
    return 301 https://$host$request_uri;
}

Reload without downtime: nginx -s reload. Then test on SSL Labs — you should get A+.

Monitoring certificate expiry

Automatic renewal can fail silently: systemd timer disabled, port 80 blocked after a firewall change, Cloudflare API token revoked… An expired certificate in production breaks the service for all your visitors.

Manual check:

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

Email alert script (to place in cron or systemd):

#!/bin/bash
# Alert if the certificate expires in less than 14 days
cert_file="/etc/letsencrypt/live/your-domain.com/fullchain.pem"
if ! openssl x509 -checkend 1209600 -noout -in "$cert_file" 2>/dev/null; then
  echo "ALERT: certificate your-domain.com expires in less than 14 days" \
    | mail -s "[SSL] Imminent expiry" [email protected]
fi

-checkend 1209600 corresponds to 14 days in seconds (14 × 86,400).

With Uptime Kuma: add a "Certificate Expiry" monitor that alerts at D-14 — no script to maintain.

Troubleshooting: rate limits, blocked port 80, DNS propagation

ACME rate limits: "too many certificates already issued"
Let's Encrypt limits to 5 certificates per registrable domain per week. If you hit this limit during testing, switch to the staging environment:

certbot --nginx --staging -d your-domain.com

Staging issues certificates not trusted by browsers, but with no rate limit. Once your configuration is validated, delete the staging certificate and reissue in production.

Port 80 blocked
The HTTP-01 challenge requires Let's Encrypt to reach http://your-domain.com/.well-known/acme-challenge/. Check:

curl -I http://your-domain.com/.well-known/acme-challenge/test
# expected: 404 (the route exists, the file doesn't yet)
ufw status | grep 80

If port 80 is blocked by an upstream firewall (cloud provider, Cloudflare in proxy mode), temporarily open it or switch to the DNS-01 challenge.

Cloudflare proxy active
If your domain is behind Cloudflare (orange cloud icon), make sure Cloudflare's SSL mode is Full (Strict) — in Flexible mode, Cloudflare communicates with your server over HTTP and nginx redirecting HTTP→HTTPS produces an infinite loop (HTTP 525/526).

Insufficient DNS propagation
For the DNS-01 challenge, wait at least 60 seconds after creating the TXT record before retrying certbot. Check:

dig TXT _acme-challenge.your-domain.com +short

If the value does not appear yet, wait before retrying.

Comparison: certbot vs acme.sh vs Caddy automatic HTTPS

Scroll the table

Criterioncertbotacme.shCaddy (auto HTTPS)
Installation`apt install certbot`Bash script, no system dependenciesSingle binary, included
Nginx/Apache integrationNative plugin (`--nginx`, `--apache`)Manual `--install-cert` hooksManages its own reverse proxy
Wildcard DNS-01Via DNS plugin (`python3-certbot-dns-*`)Native, many integrated DNS providersVia `tls.dns.*` module (Caddy v2)
RenewalSystemd timer (automatic)Cron job generated at installNative, no configuration
Learning curveLow — intuitive commandsMedium — many optionsVery low — minimalist Caddyfile
Ideal use caseExisting dedicated Nginx/Apache serverComplex environments, multi-DNS providersNew projects, integrated reverse proxy

Test before production

Before going to production, test against Let's Encrypt's staging environment (--staging with Certbot, --server letsencrypt_test with acme.sh): the production limit is 5 issuances per domain per week, and a poorly tuned script can quickly make you hit it.

Also add expiry monitoring: a probe that runs openssl x509 -checkend 604800 (7 days) and triggers an alert if automatic renewal has silently failed — a broken cron is the top cause of an HTTPS certificate expiring in the middle of the night.

Check the certbot releases page on GitHub to verify the available version before writing a tutorial or installation script.

Deploy your Cloud VPS with HTTPS ready to go

The ServOrbit Cloud VPS template comes with Docker, a reverse proxy and the Let's Encrypt chain preconfigured: certificates issued and renewed automatically, an A+ SSL Labs score from the very first deployment. Focus on your application, not on TLS plumbing.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab