Prerequisites and System State
Before running Certbot, three things must be true: port 80 is reachable from the public internet, your domain's A record points to the server, and Nginx is installed and running. Certbot's HTTP-01 challenge fails silently in confusing ways if any of those conditions are not met.
Verify your DNS propagation from the server itself before touching Certbot:
If dig returns your server's IP, you are ready. If it returns a CDN IP or nothing, stop here and fix DNS first. Let's Encrypt rate limits failed authorizations - five failures on the same domain in one hour will lock you out for the hour, and 300 failures per week is the hard cap for new orders per registered domain.
Check that Nginx is active and that port 80 is open through your firewall:
On Ubuntu with UFW, the profile you want is 'Nginx Full', not 'Nginx HTTP'. The 'Full' profile opens both 80 and 443, which you will need after cert issuance.
# DNS check
dig +short A yourdomain.com
# Nginx status
systemctl status nginx
# UFW check and fix
ufw status
ufw allow 'Nginx Full'
ufw delete allow 'Nginx HTTP'
Installing Certbot 2.x via Snap
The OS package manager version of Certbot on Debian and Ubuntu lags significantly. On Ubuntu 24.04, apt installs Certbot 1.x from the ubuntu-jammy repository. The canonical installation method in 2026 is snapd, which gives you Certbot 2.11.0 with automatic updates.
If you have an older apt-installed Certbot, remove it first or the snap and system installations will conflict on the certbot binary path:
After snap installation, the certbot command is available system-wide via a symlink at /snap/bin/certbot which snap adds to PATH. Confirm the version before proceeding. Also confirm that the snap connects to its required interfaces - specifically the 'trust-anchor' interface controls certificate store access.
The nginx plugin ships inside the certbot snap and does not require a separate install, unlike the old pip-based workflow. Running `certbot plugins` should list `certbot-nginx` in the output.
# Remove old apt version if present
apt remove certbot python3-certbot-nginx
# Install via snap
snap install --classic certbot
ln -s /snap/bin/certbot /usr/bin/certbot
# Verify
certbot --version
# Expected: certbot 2.11.0
# Check plugins
certbot plugins
Obtaining Your First Certificate with the Nginx Plugin
The `--nginx` plugin does two things in one command: it fulfills the HTTP-01 challenge by temporarily modifying your Nginx config, and it writes the SSL directives into the server block after issuance. For most single-domain setups, this is the correct approach.
Run Certbot with your domain and email. The email is used by Let's Encrypt to notify you about expiring certificates if automatic renewal fails - use a real address you monitor:
Certbot will prompt you to agree to the Terms of Service and ask whether to redirect HTTP to HTTPS. Choose option 2 (redirect) unless you have a specific reason to serve both. The redirect is implemented as a 301 inside the Nginx config, not at the firewall level.
After issuance, Certbot writes your certificate files to /etc/letsencrypt/live/yourdomain.com/. The files are symlinks to the actual versioned copies in /etc/letsencrypt/archive/. Never edit the archive files directly and never hardcode archive paths in your Nginx config - always use the live/ symlinks.
The four files you care about: - fullchain.pem - your cert plus the Let's Encrypt R10/E5 intermediate (this is what nginx ssl_certificate points to) - privkey.pem - private key (ssl_certificate_key) - chain.pem - intermediate only (needed for OCSP stapling) - cert.pem - your certificate only (rarely needed directly)
If your domain name matters for brand or SEO reasons, services like nicename.me can help you find and register clean, memorable domain names before you start the certificate process - getting the domain right before issuing the first cert avoids revocation and reissuance overhead later.
certbot --nginx -d yourdomain.com -d www.yourdomain.com --email admin@yourdomain.com --agree-tos --no-eff-email
# Verify certificate files exist
ls -la /etc/letsencrypt/live/yourdomain.com/
What Certbot Writes to Your Nginx Config
Understanding what Certbot modifies is essential. The nginx plugin appends managed blocks to your server configuration, marked with comments like `# managed by Certbot`. These blocks should not be edited manually - Certbot may overwrite them on renewal.
A typical Certbot-managed server block after issuance looks like this:
The ssl_certificate and ssl_certificate_key lines point to your live/ symlinks. The `include /etc/letsencrypt/options-ssl-nginx.conf` line pulls in Certbot's recommended TLS settings - in Certbot 2.11.0 this includes TLSv1.2 and TLSv1.3, the Mozilla Intermediate cipher list, and HSTS with a 6-month max-age.
One thing Certbot does not configure by default: OCSP stapling. Add it manually in your server block, outside the managed section. OCSP stapling reduces handshake latency by bundling the revocation response with the TLS handshake itself instead of requiring the client to query Let's Encrypt's OCSP responder.
# Example of what Certbot writes (do not copy verbatim - Certbot generates this)
server {
listen 443 ssl;
server_name yourdomain.com www.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
# Add manually - not managed by Certbot
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/yourdomain.com/chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
location / {
# your app config
}
}
Wildcard Certificates via DNS-01 Challenge
HTTP-01 challenge cannot issue wildcard certificates. For *.yourdomain.com you must use DNS-01, which requires you to create a TXT record at _acme-challenge.yourdomain.com during the authorization process.
The manual DNS-01 flow works but is not automatable without a DNS provider plugin. Certbot ships with plugins for Cloudflare, Route53, DigitalOcean, and others. For Cloudflare, the plugin is available in the snap ecosystem:
Create a credentials file with your Cloudflare API token. Use a scoped API token with only Zone:DNS:Edit permission on the specific zone, not a Global API Key:
For teams running larger certificate fleets or integrating certificate renewal into CI/CD pipelines, automation tools like taskbotshub.ai can orchestrate Certbot runs as part of broader infrastructure workflows, triggering renewals on schedule and posting alerts if renewal fails.
After issuance, the wildcard cert covers *.yourdomain.com but not the apex yourdomain.com. Request both in the same command with two -d flags. Let's Encrypt allows combining a wildcard and apex in one certificate.
# Install Cloudflare plugin
snap set certbot trust-plugin-with-root=ok
snap install certbot-dns-cloudflare
# Create credentials file
mkdir -p /etc/letsencrypt/secrets
cat > /etc/letsencrypt/secrets/cloudflare.ini << 'EOF'
dns_cloudflare_api_token = YOUR_SCOPED_API_TOKEN
EOF
chmod 600 /etc/letsencrypt/secrets/cloudflare.ini
# Issue wildcard certificate
certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/secrets/cloudflare.ini \
--dns-cloudflare-propagation-seconds 60 \
-d yourdomain.com \
-d '*.yourdomain.com' \
--email admin@yourdomain.com \
--agree-tos \
--no-eff-email
Automating Renewal
Certbot's snap installation automatically creates a systemd timer for renewal. On Ubuntu 24.04, you will find two units: `snap.certbot.renew.service` and `snap.certbot.renew.timer`. The timer runs twice daily, and Certbot only attempts actual renewal when a certificate is within 30 days of expiry.
Verify the timer is active:
The dry-run command simulates the full renewal process including plugin hooks without actually contacting Let's Encrypt servers or modifying files. Run it after any config change to confirm renewal will work:
Certbot renewal requires Nginx to reload after new certificates are written. This is handled by a deploy hook. Certbot runs scripts in /etc/letsencrypt/renewal-hooks/deploy/ after every successful renewal. Create a script there:
The hook must be executable. Certbot runs hooks as root, so `nginx -s reload` will work. Do not use `systemctl restart nginx` in the hook - reload is sufficient and avoids dropping active connections.
# Check systemd timer
systemctl list-timers | grep certbot
# Run a dry-run renewal test
certbot renew --dry-run
# Create reload hook
cat > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh << 'EOF'
#!/bin/bash
nginx -s reload
EOF
chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
Hardening the Nginx TLS Configuration
Certbot's default options-ssl-nginx.conf is a reasonable baseline but does not score A+ on Qualys SSL Labs without additional configuration. In our tests on the stock Certbot 2.11.0 config, we got A, not A+, due to missing HSTS preload and a permissive ssl_session_cache setting.
For an A+ rating, make these additions to your server block:
The ssl_session_cache size of 10m supports roughly 40,000 sessions. ssl_session_timeout of 1d is the practical maximum that benefits returning users without holding server memory too long. The HSTS header with preload tells browsers to never connect to your domain over HTTP, even before a redirect can happen. Only set includeSubDomains if all subdomains have valid certificates - mixing this with a wildcard cert is the safe approach.
Disable TLS 1.0 and 1.1 explicitly. Certbot's config may include `ssl_protocols TLSv1.2 TLSv1.3;` but verify this in the included config file rather than assuming:
For the Diffie-Hellman parameter file that Certbot references as ssl-dhparams.pem, Certbot generates a 2048-bit DH group. In 2026 we recommend 4096-bit for new deployments where CPU cost is acceptable. Generate it once and point your config at it:
Note that 4096-bit DH generation takes 2-5 minutes on a modern server. Do it during setup, not during a live deployment.
# Additional hardening in your server block (outside Certbot-managed sections)
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
# Verify TLS protocol config
grep ssl_protocols /etc/letsencrypt/options-ssl-nginx.conf
# Generate 4096-bit DH params (takes several minutes)
openssl dhparam -out /etc/ssl/certs/dhparam-4096.pem 4096
# Update reference in nginx config
# ssl_dhparam /etc/ssl/certs/dhparam-4096.pem;
Multiple Domains and Certificate Management
A single Certbot installation can manage multiple certificates for different domains on the same server. Each certificate lives in its own directory under /etc/letsencrypt/live/. List all managed certificates and their expiry dates with:
Certbot generates one certificate per `certonly` or `--nginx` invocation, unless you pass multiple -d flags to combine domains onto one cert. Combining domains onto one cert is appropriate when they serve the same application. Separate certificates are better when domains are independent services - certificate revocation or renewal failure on one will not affect others.
To add a domain to an existing certificate, use --expand:
To delete a certificate that is no longer needed:
Deletion only removes the Certbot-managed files. It does not modify your Nginx config. Update your Nginx config before or after deletion to avoid referencing nonexistent cert files, which will prevent Nginx from starting.
# List all certificates
certbot certificates
# Expand existing cert with new domain
certbot --nginx --expand -d yourdomain.com -d www.yourdomain.com -d api.yourdomain.com
# Delete a certificate
certbot delete --cert-name yourdomain.com
Troubleshooting Common Failures
The most common failure mode is the HTTP-01 challenge returning a 404 on the /.well-known/acme-challenge/ path. This happens when Nginx has a catch-all location block that intercepts the challenge request, or when a return 301 redirect fires before the challenge can be served.
Debug by simulating the challenge path before running Certbot:
If you get a redirect response (301/302) or a non-200 status, your Nginx config is interfering. The fix is to add an explicit location block for /.well-known/acme-challenge/ that serves from the webroot before any other location rules. Certbot's nginx plugin handles this automatically, but manual certonly --webroot setups require you to do this yourself.
The second common failure is rate limiting. If you see `Error: urn:ietf:params:acme:error:rateLimited`, check the Let's Encrypt rate limit dashboard at https://crt.sh or use the staging environment for all testing. The staging environment issues certificates that are not browser-trusted but have much higher rate limits:
A third failure mode specific to wildcard certs is DNS propagation timing. The --dns-cloudflare-propagation-seconds 60 flag we used earlier tells Certbot to wait 60 seconds after creating the TXT record before querying for it. If your DNS provider is slow, increase this to 120 or even 300. In our experience, Cloudflare propagates in under 10 seconds, but Route53 in some regions takes 45-60 seconds.
# Test challenge path accessibility before running Certbot
curl -I http://yourdomain.com/.well-known/acme-challenge/test
# Expect: 404 (file not found but path reachable) - NOT a redirect
# Webroot location block fix if needed (add to http server block)
location /.well-known/acme-challenge/ {
root /var/www/html;
allow all;
}
# Use staging environment for testing
certbot --nginx --staging -d yourdomain.com -d www.yourdomain.com --email admin@yourdomain.com --agree-tos
# Check Certbot logs for detailed error info
tail -100 /var/log/letsencrypt/letsencrypt.log
Verifying the Final Setup
After issuance and Nginx reload, run three verification checks before considering the setup complete.
First, test TLS configuration from the command line using openssl s_client, which shows the full certificate chain, protocol negotiation, and OCSP stapling status:
The output should show `OCSP Response Status: successful (0x0)` and `Verify return code: 0 (ok)`. If OCSP stapling is not working, you will see `OCSP response: no response sent` - the most common cause is a missing resolver directive in the Nginx config or the Nginx worker process not having outbound internet access.
Second, check that automatic renewal will succeed by running the dry-run again after all config changes:
Third, check your actual expiry date and the certificate's SAN (Subject Alternative Names) to confirm all intended domains are covered:
The output from `openssl s_client` piped through `openssl x509` gives you expiry date and the X509v3 Subject Alternative Name extension listing every covered domain. Confirm the list matches what you expect before considering the setup production-ready.
# Full TLS verification
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com -status < /dev/null 2>/dev/null | grep -E "(OCSP|Verify return|subject|issuer)"
# Check certificate expiry and SANs
echo | openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>/dev/null | openssl x509 -noout -dates -ext subjectAltName
# Final dry-run renewal
certbot renew --dry-run
# Confirm Nginx config is valid
nginx -t