Every website and application served over the internet should use HTTPS. Let's Encrypt provides free, automated TLS certificates, and Certbot is the official client that handles obtaining, installing, and renewing those certificates on your Ubuntu VPS. There are no excuses left for serving traffic over plain HTTP.

This guide covers the complete Let's Encrypt workflow on Ubuntu 24.04: installing Certbot, obtaining certificates for both Nginx and Apache, configuring automatic renewal, generating wildcard certificates with DNS validation, securing multiple domains on a single server, testing your SSL configuration, and troubleshooting common issues. By the end, your sites will score an A+ on SSL Labs.

Prerequisites

Before starting, you need:

MassiveGRID Ubuntu VPS — Ubuntu 24.04 LTS pre-installed, Proxmox HA cluster with automatic failover, Ceph 3x replicated NVMe storage, independent CPU/RAM/storage scaling, 12 Tbps DDoS protection, 4 global datacenter locations, 100% uptime SLA, and 24/7 human support rated 9.5/10. Deploy a Cloud VPS from $1.99/mo.

How Let's Encrypt Works

Let's Encrypt uses the ACME (Automatic Certificate Management Environment) protocol to verify that you control the domain you're requesting a certificate for. The two primary validation methods are:

Let's Encrypt certificates are valid for 90 days. Certbot sets up automatic renewal so you never have to think about expiration after the initial setup.

Installing Certbot on Ubuntu 24.04

The recommended installation method is via snap, which ensures you always get the latest version of Certbot with automatic updates.

First, make sure snapd is installed and up to date:

sudo snap install core
sudo snap refresh core

Remove any older OS-packaged version of Certbot to avoid conflicts:

sudo apt remove certbot -y

Install Certbot via snap:

sudo snap install --classic certbot

Create a symbolic link so you can run certbot from any directory:

sudo ln -s /snap/bin/certbot /usr/bin/certbot

Verify the installation:

certbot --version
# certbot 3.1.0

Obtaining a Certificate for Nginx

If you're running Nginx, Certbot's Nginx plugin handles everything automatically — it obtains the certificate, modifies your Nginx configuration to use it, and sets up HTTPS redirection.

Make sure your Nginx server block has the correct server_name directive. For example, in /etc/nginx/sites-available/yourdomain.com:

server {
    listen 80;
    listen [::]:80;
    server_name yourdomain.com www.yourdomain.com;

    root /var/www/yourdomain.com/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

Enable the site and test the configuration:

sudo ln -s /etc/nginx/sites-available/yourdomain.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Now run Certbot with the Nginx plugin:

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

Certbot will:

  1. Verify domain ownership via the HTTP-01 challenge
  2. Obtain the certificate from Let's Encrypt
  3. Modify your Nginx server block to include SSL configuration
  4. Ask if you want to redirect HTTP to HTTPS (select option 2 — redirect)
  5. Reload Nginx with the new configuration

After Certbot finishes, your Nginx configuration will be updated automatically. You can inspect the changes:

cat /etc/nginx/sites-available/yourdomain.com

Certbot adds a new server block listening on port 443 with the certificate paths and SSL parameters, plus a redirect block that sends all HTTP traffic to HTTPS.

Certificate File Locations

Certbot stores certificates in /etc/letsencrypt/live/yourdomain.com/:

File Description
fullchain.pem Your certificate + intermediate certificates (what Nginx uses for ssl_certificate)
privkey.pem Your private key (what Nginx uses for ssl_certificate_key)
cert.pem Your certificate only
chain.pem Intermediate certificates only

These are symbolic links managed by Certbot. They always point to the latest certificate, even after renewal.

Obtaining a Certificate for Apache

If you're using Apache instead of Nginx, install the Apache plugin for Certbot:

sudo snap install --classic certbot
sudo apt install -y python3-certbot-apache

Ensure your Apache virtual host has the correct ServerName. In /etc/apache2/sites-available/yourdomain.com.conf:

<VirtualHost *:80>
    ServerName yourdomain.com
    ServerAlias www.yourdomain.com
    DocumentRoot /var/www/yourdomain.com/html

    <Directory /var/www/yourdomain.com/html>
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/yourdomain-error.log
    CustomLog ${APACHE_LOG_DIR}/yourdomain-access.log combined
</VirtualHost>

Enable the site and required modules:

sudo a2ensite yourdomain.com.conf
sudo a2enmod ssl
sudo a2enmod rewrite
sudo systemctl reload apache2

Run Certbot with the Apache plugin:

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

Certbot creates a new SSL virtual host configuration and enables HTTPS redirection, then reloads Apache automatically.

Standalone Mode (No Web Server)

If you don't have a web server running — for example, you're using Certbot to obtain a certificate for a mail server, database, or other non-web service — use standalone mode. Certbot temporarily starts its own web server on port 80:

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

Important: Port 80 must be free when using standalone mode. If Nginx or Apache is running, stop it first or use the appropriate plugin instead.

You can also use port 443 for standalone mode with the TLS-ALPN-01 challenge:

sudo certbot certonly --standalone --preferred-challenges tls-alpn-01 -d mail.yourdomain.com

Webroot Mode

Webroot mode is useful when you don't want Certbot to modify your web server configuration at all. Certbot places the challenge file in your web server's document root, and you manage the SSL configuration yourself:

sudo certbot certonly --webroot -w /var/www/yourdomain.com/html -d yourdomain.com -d www.yourdomain.com

This only obtains the certificate. You must configure your web server to use it manually. In Nginx:

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

    # SSL settings
    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;

    root /var/www/yourdomain.com/html;
    index index.html;
}

Automatic Renewal

Certbot sets up automatic renewal during installation. On Ubuntu 24.04 with the snap package, renewal is handled by a systemd timer.

Check the timer status:

sudo systemctl status snap.certbot.renew.timer

You should see output indicating the timer is active and scheduled:

● snap.certbot.renew.timer - Timer renew for snap application certbot.renew
     Loaded: loaded (/etc/systemd/system/snap.certbot.renew.timer; enabled)
     Active: active (waiting)
    Trigger: Wed 2026-02-28 05:22:00 UTC; 11h left

The timer runs twice daily and only attempts renewal when a certificate is within 30 days of expiration. Since certificates are valid for 90 days, this gives you plenty of buffer.

Test that renewal works without actually renewing:

sudo certbot renew --dry-run

If the dry run succeeds, your certificates will renew automatically. If it fails, the error output will tell you exactly what to fix.

Renewal Hooks

After a certificate is renewed, your web server needs to reload its configuration to pick up the new certificate. Certbot handles this automatically for Nginx and Apache plugins, but if you used certonly mode, you need to set up a renewal hook.

Create a deploy hook that reloads Nginx after renewal:

sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
sudo nano /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

Add the following:

#!/bin/bash
systemctl reload nginx

Make it executable:

sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

You can also specify hooks directly in the renewal configuration file at /etc/letsencrypt/renewal/yourdomain.com.conf:

[renewalparams]
# ... existing settings ...

[[ post-hook ]]
post_hook = systemctl reload nginx

Wildcard Certificates with DNS Validation

A wildcard certificate covers all subdomains of a domain — *.yourdomain.com matches app.yourdomain.com, api.yourdomain.com, staging.yourdomain.com, and any other subdomain. Wildcard certificates require DNS-01 validation.

Manual DNS Validation

Request a wildcard certificate:

sudo certbot certonly --manual --preferred-challenges dns \
  -d "yourdomain.com" \
  -d "*.yourdomain.com"

Certbot will ask you to create a DNS TXT record. You'll see output like:

Please deploy a DNS TXT record under the name:

_acme-challenge.yourdomain.com

with the following value:

gfj9Xq...Rg5nTzs

Before continuing, verify the TXT record has been deployed.
Press Enter to continue...

Log in to your DNS provider and create the TXT record. Then verify it has propagated before pressing Enter:

dig -t TXT _acme-challenge.yourdomain.com +short

Wait until the correct value appears, then press Enter in the Certbot prompt. Note that if you're obtaining both yourdomain.com and *.yourdomain.com, Certbot will ask you to create two TXT records (both under _acme-challenge.yourdomain.com).

Important: Manual DNS validation does not support automatic renewal. Every 90 days, you'll need to manually update the TXT record. For automated wildcard renewal, use a DNS plugin.

Automated DNS Validation with Cloudflare

If your DNS is managed by Cloudflare, you can automate DNS validation using the Certbot Cloudflare plugin:

sudo snap set certbot trust-plugin-with-root=ok
sudo snap install certbot-dns-cloudflare

Create a Cloudflare API credentials file:

sudo mkdir -p /etc/letsencrypt/secrets
sudo nano /etc/letsencrypt/secrets/cloudflare.ini

Add your Cloudflare API token (create a token with Zone:DNS:Edit permission in the Cloudflare dashboard):

# Cloudflare API token
dns_cloudflare_api_token = your-cloudflare-api-token-here

Secure the credentials file:

sudo chmod 600 /etc/letsencrypt/secrets/cloudflare.ini

Now request the wildcard certificate with automatic DNS validation:

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

This method fully supports automatic renewal. Certbot will create and clean up the DNS TXT record automatically during each renewal cycle.

Other DNS Plugins

Certbot has DNS plugins for many providers:

Provider Plugin Package
Cloudflare certbot-dns-cloudflare
DigitalOcean certbot-dns-digitalocean
Route 53 (AWS) certbot-dns-route53
Google Cloud DNS certbot-dns-google
Linode certbot-dns-linode
OVH certbot-dns-ovh

Install them via snap with the same pattern: sudo snap install certbot-dns-PROVIDER.

Multi-Domain Certificates (SAN)

A single certificate can cover multiple different domains using Subject Alternative Names (SAN). This is useful when you host several sites on one server and want to manage fewer certificates:

sudo certbot --nginx \
  -d yourdomain.com \
  -d www.yourdomain.com \
  -d app.yourdomain.com \
  -d anotherdomain.com \
  -d www.anotherdomain.com

All listed domains will be included in a single certificate. Each domain must pass the validation challenge — make sure all DNS records point to your server and all domains are configured in Nginx.

To add a domain to an existing certificate, use --expand:

sudo certbot --nginx --expand \
  -d yourdomain.com \
  -d www.yourdomain.com \
  -d app.yourdomain.com \
  -d newdomain.com

You must include all existing domains plus the new one. Certbot issues a replacement certificate covering all listed domains.

Hardening Your SSL Configuration

The default Certbot configuration is good but can be improved for maximum security. After Certbot has configured your Nginx server block, add these settings inside the server block listening on port 443:

Protocols and Ciphers

# Only allow TLS 1.2 and 1.3 (disable older protocols)
ssl_protocols TLSv1.2 TLSv1.3;

# Use strong cipher suites
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;

# Let the server choose the cipher order
ssl_prefer_server_ciphers off;

OCSP Stapling

OCSP stapling improves SSL performance by allowing the server to send the certificate's revocation status directly, instead of requiring the client to check with the certificate authority:

# Enable OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;

# Use Google and Cloudflare DNS for OCSP resolution
resolver 8.8.8.8 1.1.1.1 valid=300s;
resolver_timeout 5s;

SSL Session Caching

SSL session caching reduces the overhead of repeated TLS handshakes from the same clients:

# Session cache shared between all worker processes
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;

# Disable session tickets (for forward secrecy)
ssl_session_tickets off;

Security Headers

Add HTTP security headers to your HTTPS server block:

# Enforce HTTPS for 2 years (with subdomains and preload)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

# Prevent clickjacking
add_header X-Frame-Options "SAMEORIGIN" always;

# Prevent MIME type sniffing
add_header X-Content-Type-Options "nosniff" always;

# Control referrer information
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# Basic permissions policy
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

Diffie-Hellman Parameters

Generate a strong Diffie-Hellman parameter file for additional security:

sudo openssl dhparam -out /etc/letsencrypt/ssl-dhparams-4096.pem 4096

This command takes several minutes. Then reference it in your Nginx configuration:

ssl_dhparam /etc/letsencrypt/ssl-dhparams-4096.pem;

Reload Nginx after making changes:

sudo nginx -t && sudo systemctl reload nginx

Complete Hardened Nginx SSL Configuration

Here is a complete Nginx server block with all the hardening options applied:

# HTTP — redirect all traffic to HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name yourdomain.com www.yourdomain.com;
    return 301 https://$server_name$request_uri;
}

# HTTPS
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;

    # Let's Encrypt certificate
    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

    # Protocols and ciphers
    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:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
    ssl_prefer_server_ciphers off;

    # DH parameters
    ssl_dhparam /etc/letsencrypt/ssl-dhparams-4096.pem;

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

    # Session caching
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # Security headers
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

    # Document root
    root /var/www/yourdomain.com/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    # Block dotfiles
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }
}

Testing Your SSL Configuration

SSL Labs Test

The most authoritative SSL test is Qualys SSL Labs. Open your browser and go to:

https://www.ssllabs.com/ssltest/analyze.html?d=yourdomain.com

With the hardened configuration above, you should score an A+ rating. The test checks protocol support, key exchange strength, cipher suite quality, and certificate chain validity.

Command-Line Testing

Test your SSL configuration locally with openssl:

# Check certificate details
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com < /dev/null 2>/dev/null | openssl x509 -text -noout

Check the expiration date:

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

Verify the certificate chain:

openssl s_client -connect yourdomain.com:443 -servername yourdomain.com -showcerts < /dev/null

Test specific TLS versions:

# This should succeed (TLS 1.3)
openssl s_client -connect yourdomain.com:443 -tls1_3

# This should fail (TLS 1.1 disabled)
openssl s_client -connect yourdomain.com:443 -tls1_1

Using Certbot to Check Certificate Status

List all certificates managed by Certbot:

sudo certbot certificates

This shows each certificate's domains, expiry date, certificate path, and private key path. Use this to quickly verify which certificates are installed and when they expire.

Managing Certificates

Revoking a Certificate

If a certificate's private key is compromised or you no longer use a domain, revoke the certificate:

sudo certbot revoke --cert-path /etc/letsencrypt/live/yourdomain.com/fullchain.pem

Certbot will ask if you also want to delete the certificate files. It's usually safe to say yes.

Deleting Certificate Files

To remove a certificate without revoking it (e.g., you've already moved the domain to a different server):

sudo certbot delete --cert-name yourdomain.com

Renewing a Specific Certificate

Force renewal of a specific certificate (useful for testing):

sudo certbot renew --cert-name yourdomain.com --force-renewal

Caution: Don't use --force-renewal frequently. Let's Encrypt has rate limits (50 certificates per registered domain per week). The --dry-run flag doesn't count against rate limits.

Troubleshooting Common Issues

Port 80 Blocked by Firewall

The HTTP-01 challenge requires port 80 to be open. If Certbot fails with a connection error, check your firewall:

# Check UFW status
sudo ufw status

# Allow HTTP traffic
sudo ufw allow 80/tcp

# Also allow HTTPS
sudo ufw allow 443/tcp

If you're using iptables directly:

sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT

Also check if your cloud provider has a separate firewall or security group that blocks port 80.

DNS Record Not Pointing to Server

The HTTP-01 challenge requires the domain's A record to resolve to your server's IP. Verify:

dig +short A yourdomain.com
# Should return your server's IP address

If the DNS record is correct but recently changed, wait for propagation (up to 48 hours, though typically 5-30 minutes).

Rate Limits

Let's Encrypt enforces rate limits to prevent abuse. The main limits are:

Limit Value Period
Certificates per registered domain 50 Per week
Duplicate certificates 5 Per week
Failed validations 5 Per hour per account per hostname
Accounts per IP 10 Per 3 hours

If you hit a rate limit, you'll need to wait until the window expires. Use the staging environment for testing to avoid hitting production limits:

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

Staging certificates are not trusted by browsers, but the issuance process is identical. Once your setup works with staging, switch to production by removing the --staging flag and running again with --force-renewal.

Nginx Configuration Errors

If Certbot fails while modifying Nginx configuration, check for syntax errors:

sudo nginx -t

Common issues include duplicate server_name directives or conflicting listen directives across server blocks. Fix any errors, then re-run Certbot.

Certbot Cannot Find Your Server Block

Certbot looks for server blocks in /etc/nginx/sites-enabled/ with a server_name matching the domain you specified. If Certbot says "Unable to find a matching server block," verify:

Permission Denied

Certbot must run as root or with sudo. The /etc/letsencrypt/ directory is owned by root with restricted permissions. Never change ownership of this directory.

Certificate Renewal Failing

If automatic renewal stops working, check the renewal configuration:

cat /etc/letsencrypt/renewal/yourdomain.com.conf

Common causes of renewal failure:

Test renewal manually:

sudo certbot renew --dry-run

Automating SSL Monitoring

Even with automatic renewal, it's wise to monitor your certificates. Create a simple script that checks expiration and alerts you if a certificate is expiring within 14 days:

sudo nano /usr/local/bin/check-ssl.sh
#!/bin/bash
DOMAIN="yourdomain.com"
DAYS_WARNING=14
EMAIL="admin@yourdomain.com"

EXPIRY=$(echo | openssl s_client -connect "$DOMAIN:443" -servername "$DOMAIN" 2>/dev/null \
  | openssl x509 -noout -enddate 2>/dev/null \
  | cut -d= -f2)

if [ -z "$EXPIRY" ]; then
  echo "ERROR: Could not retrieve certificate for $DOMAIN" | mail -s "SSL Check Failed: $DOMAIN" "$EMAIL"
  exit 1
fi

EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))

if [ "$DAYS_LEFT" -lt "$DAYS_WARNING" ]; then
  echo "WARNING: SSL certificate for $DOMAIN expires in $DAYS_LEFT days (on $EXPIRY)" \
    | mail -s "SSL Expiring Soon: $DOMAIN" "$EMAIL"
fi
sudo chmod +x /usr/local/bin/check-ssl.sh

Schedule this as a daily cron job (see our cron jobs and task scheduling guide for more details):

sudo crontab -e
# Add:
0 9 * * * /usr/local/bin/check-ssl.sh

For comprehensive monitoring, including uptime, resource usage, and alerting, refer to our Ubuntu VPS monitoring guide.

SSL for Reverse Proxy Setups

If you're running applications behind Nginx as a reverse proxy (Node.js, Python, Docker containers, etc.), SSL termination happens at Nginx. The backend application communicates with Nginx over plain HTTP on localhost.

Here's a typical reverse proxy setup with SSL. For a deeper guide, see our Nginx reverse proxy tutorial:

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

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name app.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/app.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.yourdomain.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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;
    }
}

Running multiple applications on different subdomains? Each subdomain gets its own Certbot certificate or you can use a wildcard certificate to cover them all. If you're deploying Docker containers with automatic SSL, consider using Traefik as described in our Traefik Docker guide.

Prefer Managed SSL?

If managing SSL certificates, renewal hooks, web server configuration, and security headers feels like too much operational overhead, consider MassiveGRID's Managed Dedicated Cloud Servers. The managed service handles SSL certificate provisioning, automatic renewal, web server hardening, security patches, and 24/7 monitoring — so you can focus on your application instead of infrastructure. Every managed server runs on a Proxmox HA cluster with automatic failover and Ceph triple-replicated NVMe storage for enterprise-grade reliability.

What's Next