How to Set Up a Free SSL Certificate on Linux
By the end of this guide you will have a valid, auto-renewing HTTPS certificate issued by Let's Encrypt installed on an Nginx web server running Ubuntu 24.04. Certbot handles the ACME challenge, rewrites your Nginx config to redirect HTTP to HTTPS, and installs a systemd timer that renews the certificate before it expires. No paid certificate authority, no manual renewal.
Prerequisites
- Ubuntu 24.04 server with a public IP address
- A domain name (e.g.,
example.com) with an A record pointing to that IP - Nginx installed and serving at least a default virtual host on port 80
- Root or
sudoaccess - Ports 80 and 443 open in your firewall
1. Update the Package Index
Run a full package index refresh before installing anything.
sudo apt update
Expected output ends with a line similar to:
Reading package lists... Done
2. Install Certbot and the Nginx Plugin
Install Certbot together with its Nginx plugin. The plugin reads your existing Nginx configuration, automates the ACME HTTP-01 challenge, and edits your server blocks to enable TLS.
sudo apt install -y certbot python3-certbot-nginx
Verify the installed version:
certbot --version
certbot 2.9.0
(Your minor version may differ; anything in the 2.x line is current as of Ubuntu 24.04.)
3. Confirm Nginx Is Running and Configured for Your Domain
Before requesting a certificate, Certbot must be able to place a temporary file under your domain's web root and have Let's Encrypt fetch it over HTTP. Confirm Nginx is active:
sudo systemctl status nginx
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled)
Active: active (running)
Also verify your domain resolves to this server's IP before proceeding:
dig +short example.com
The output must be your server's public IP. If it is not, fix your DNS record and wait for propagation before continuing.
4. Open Ports 80 and 443 in UFW
Let's Encrypt's HTTP-01 challenge requires port 80. TLS traffic requires port 443. Allow both through UFW:
sudo ufw allow 'Nginx Full'
Rule added
Rule added (v6)
If you previously allowed only Nginx HTTP, remove the redundant rule:
sudo ufw delete allow 'Nginx HTTP'
Check the active rules:
sudo ufw status
To Action From
-- ------ ----
Nginx Full ALLOW Anywhere
Nginx Full (v6) ALLOW Anywhere (v6)
5. Request and Install the Certificate
Run Certbot with the --nginx plugin flag. Replace example.com and www.example.com with your actual domain names. The -d flag accepts multiple comma-separated or repeated values.
sudo certbot --nginx -d example.com -d www.example.com
Certbot will:
- Ask for an email address for expiry notices — enter a real one.
- Ask you to agree to the Let's Encrypt Subscriber Agreement — type
Y. - Ask whether to share your email with EFF — your choice, not required.
- Perform the HTTP-01 challenge automatically.
- Rewrite your Nginx server block to add TLS directives and an HTTP→HTTPS redirect.
Successful output ends with:
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/example.com/fullchain.pem
Key is saved at: /etc/letsencrypt/live/example.com/privkey.pem
This certificate expires on 2025-10-01.
Deploying certificate to VirtualHost /etc/nginx/sites-enabled/example.com
Redirecting all traffic on port 80 to ssl in /etc/nginx/sites-enabled/example.com
6. Inspect the Modified Nginx Configuration
Certbot edits your server block in place. Review what it wrote:
sudo cat /etc/nginx/sites-enabled/example.com
You should see two server blocks. The first listens on port 443 and includes lines like:
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
The second block listens on port 80 and issues a 301 redirect to HTTPS. If your site had no named server block, Certbot will have modified /etc/nginx/sites-enabled/default instead.
Test the configuration syntax before reloading:
sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Reload Nginx to apply any pending changes:
sudo systemctl reload nginx
7. Confirm Auto-Renewal Is Active
Let's Encrypt certificates expire after 90 days. Certbot installs a systemd timer at installation time that runs certbot renew twice daily. Confirm it is enabled:
sudo systemctl status certbot.timer
● certbot.timer - Run certbot twice daily
Loaded: loaded (/usr/lib/systemd/system/certbot.timer; enabled)
Active: active (waiting)
Trigger: Thu 2025-07-03 12:00:00 UTC; 5h left
Do a dry-run to confirm the renewal process completes without errors:
sudo certbot renew --dry-run
Congratulations, all simulated renewals succeeded:
/etc/letsencrypt/live/example.com/fullchain.pem (success)
If the dry-run succeeds, your certificate will renew automatically with no further action required.
Verify It Works
Run three checks to confirm the certificate is live and trusted.
Check 1 — curl over HTTPS:
curl -I https://example.com
HTTP/2 200
server: nginx
...
A 200 response over HTTP/2 with no certificate error confirms TLS is working.
Check 2 — HTTP redirects to HTTPS:
curl -I http://example.com
HTTP/1.1 301 Moved Permanently
Location: https://example.com/
Check 3 — Certificate details:
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null \
| openssl x509 -noout -issuer -dates
issuer=C=US, O=Let's Encrypt, CN=R11
notBefore=Jul 1 00:00:00 2025 GMT
notAfter=Sep 29 00:00:00 2025 GMT
The issuer must show Let's Encrypt and notAfter must be roughly 90 days from today.
Troubleshooting
Certbot fails with "Connection refused" or "Timeout during connect"
Port 80 is blocked. Re-check UFW (sudo ufw status) and your cloud provider's security group or firewall rules. The Let's Encrypt servers must reach port 80 on your IP.
"No names were found in your configuration files" error
Certbot found no server_name directive in any enabled Nginx config. Add server_name example.com www.example.com; to your server block, run sudo nginx -t && sudo systemctl reload nginx, then retry.
DNS resolution returns the wrong IP
Your A record has not propagated yet, or points to the wrong host. Run dig +short example.com @8.8.8.8 to query Google's resolver directly. Do not run Certbot until DNS is correct — failed challenges count against Let's Encrypt's rate limits (5 failures per domain per hour).
nginx -t fails after Certbot edits the config
Open the edited server block and look for duplicate listen directives or a missing semicolon. Certbot occasionally duplicates a listen 443 ssl line if the block already had one. Remove the duplicate, re-test, and reload.
certbot renew --dry-run reports "Problem binding to port 80"
Another process is using port 80 during the standalone challenge. With the --nginx plugin this should not happen, but if you switched plugins, ensure you are using --nginx and not --standalone. Check with sudo ss -tlnp | grep :80.
Certificate shows as expired in the browser despite a successful renewal
Nginx is serving a cached response or the old certificate file. Run sudo systemctl reload nginx after any renewal. The Certbot deploy hook at /etc/letsencrypt/renewal-hooks/deploy/ should handle this automatically; confirm a reload hook exists:
ls /etc/letsencrypt/renewal-hooks/deploy/
If the directory is empty, create the hook:
sudo bash -c 'echo -e "#!/bin/sh\nsystemctl reload nginx" > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh'
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
Next Steps
With your free SSL certificate on Linux running and auto-renewing, consider these follow-on hardening tasks:
- Disable weak TLS versions. Edit
/etc/letsencrypt/options-ssl-nginx.confand setssl_protocols TLSv1.2 TLSv1.3;if TLSv1 or TLSv1.1 are still present. - Add HSTS. Insert
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;in your HTTPS server block to instruct browsers to never use plain HTTP. - Test your SSL configuration. Run your domain through SSL Labs to catch cipher or protocol issues before users do.
- Wildcard certificates. If you need
*.example.com, Certbot supports DNS-01 challenges via DNS provider plugins (e.g.,python3-certbot-dns-cloudflare). The process is similar but requires API credentials for your DNS provider.
Certbot's auto-renewal and Let's Encrypt's 90-day cycle mean you have one less recurring cost and one less manual task on your $40/month stack.