Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

How to Install an SSL Certificate on Nginx: Let's Encrypt and Manual Paths

Sean

Platform Writer

Jul 06, 2026
7 min read

Install an SSL certificate on Nginx with certbot for a free Let’s Encrypt cert (auto-renewed), or with a manual path for a custom CA. The right certbot invocation is certbot --nginx -d example.com -d www.example.com. The right manual path is a server block with ssl_certificate and ssl_certificate_key. The team that gets both right has HTTPS that renews itself.

How to Install an SSL Certificate on Nginx: Let's Encrypt and Manual Paths

Table of contents

The Let’s Encrypt path with certbot

The right tool for a free, auto-renewing SSL cert on Nginx is certbot from the EFF. The install on Ubuntu or Debian is:

sudo apt install certbot python3-certbot-nginx

On RHEL, Fedora, and CentOS Stream, the equivalent is:

sudo dnf install certbot python3-certbot-nginx

The right certbot invocation to install a cert and configure Nginx automatically is:

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

Certbot will ask for an email address (for renewal notifications), agree to the terms of service, and then run the ACME challenge. The ACME challenge is a request to Let’s Encrypt’s servers to verify that you control the domain. Certbot handles this by temporarily modifying the Nginx config to serve a specific URL, asking Let’s Encrypt to fetch it, and then cleaning up. After the challenge passes, Let’s Encrypt issues the cert, and certbot writes the SSL config into your Nginx server block.

The cert is stored in /etc/letsencrypt/live/example.com/. The full chain is fullchain.pem and the private key is privkey.pem. Certbot will automatically set up a systemd timer or cron job to renew the cert before it expires. The default expiry is 90 days, and the auto-renewal runs twice a day. The right verification is sudo certbot renew --dry-run which simulates the renewal without actually doing it.

The manual path for a custom CA

If you have a cert from a commercial CA (DigiCert, Sectigo, Let’s Encrypt via a non-certbot path, or an internal CA), the right approach is to drop the cert files into a known location and write the Nginx config by hand. The conventional location is /etc/nginx/ssl/:

sudo mkdir -p /etc/nginx/ssl/example.com
sudo cp fullchain.pem /etc/nginx/ssl/example.com/
sudo cp privkey.pem /etc/nginx/ssl/example.com/
sudo chmod 600 /etc/nginx/ssl/example.com/privkey.pem

The Nginx server block:

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    ssl_certificate /etc/nginx/ssl/example.com/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers on;

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

The ssl_protocols TLSv1.2 TLSv1.3 line disables TLS 1.0 and TLS 1.1, which are deprecated. The ssl_ciphers line picks modern cipher suites. The right answer for most sites is the default cipher list, which Nginx negotiates with the client’s supported list. Hardcoding a cipher list is the right answer if you have a specific compliance requirement.

Forcing HTTPS redirect

The right Nginx config redirects all HTTP traffic to HTTPS. Add a second server block listening on port 80:

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

The 301 redirect tells browsers and search engines that this is a permanent move, which is the right answer for a site that has always-on HTTPS. The right verification is to visit http://example.com and confirm the browser ends up on https://example.com with a lock icon in the address bar.

The wrong answer is to use a 302 redirect (temporary). Search engines treat 302 as ‘maybe permanent’ and will not consolidate the page rank. The right answer for an HTTPS-only site is 301.

Verifying the SSL config is correct

The right verification is sudo nginx -t. This tests the Nginx config and reports any syntax errors. A successful test prints:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

After the test passes, reload Nginx:

sudo systemctl reload nginx

The wrong answer is to restart Nginx. A restart drops all in-flight connections. A reload tells Nginx to re-read the config without dropping connections. The right answer is always reload unless you have a reason to actually stop and start the process.

The other verification is from a client. The right tool is the SSL Labs test (ssllabs.com/ssltest). It runs a comprehensive scan of the SSL config, including the cert chain, the supported protocols, the cipher suites, and known vulnerabilities. The right grade is A or A+. Anything below A means there is a configuration issue to fix.

Common mistakes and how to avoid them

The most common mistake is the wrong certificate chain. The fullchain.pem includes the intermediate certificate, which most clients need to validate the leaf. If you only use the leaf cert (sometimes called cert.pem or example.com.crt), some clients will fail with unable to get local issuer certificate errors. The right answer is fullchain.pem, not the leaf alone.

The second most common mistake is forgetting to set the right file permissions. The private key should be readable only by root. The right permissions are chmod 600 privkey.pem and chown root:root privkey.pem. Nginx refuses to start if the private key is world-readable.

The third most common mistake is not testing HTTP/2. Modern browsers prefer HTTP/2 over HTTP/1.1 because of multiplexing and header compression. The right answer is listen 443 ssl http2;. The http2 keyword enables HTTP/2 for the listener. Note that Nginx 1.25.1 and later use a different syntax (http2 on; directive inside the server block), but the listen 443 ssl http2; syntax still works for backward compatibility.

Auto-renewal with systemd timer

The right way to handle cert renewal is a systemd timer that runs certbot renew twice a day. Certbot’s package installs the timer by default. To verify:

sudo systemctl list-timers | grep certbot

The timer should be listed. To test the renewal:

sudo certbot renew --dry-run

If the dry run succeeds, the real renewal will succeed. The right answer is to set up a notification email so you get an alert if the renewal fails. Certbot supports this through the --email flag on the initial cert request.

A common gotcha is that the cert renews but Nginx does not reload, which means the new cert is not served. The right answer is to use the --deploy-hook flag on certbot renew to trigger a reload:

sudo certbot renew --deploy-hook 'systemctl reload nginx'

Or to put the same in the certbot config file at /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh.

FAQ

Do I need both www and the apex in the cert?

Yes if you serve both. If you only serve example.com (and www.example.com redirects), the cert can be just for the apex. The right answer is to include both as Subject Alternative Names (SANs) in the cert, which is what certbot’s -d flag does.

What is the difference between RSA and ECDSA certs?

Both are valid for TLS. ECDSA certs are smaller and faster to compute, but some very old clients (Windows XP, Java 6) do not support them. The right answer for most sites is to issue a dual cert (RSA + ECDSA) and let the server pick. Certbot does not support this directly; the right answer is to issue both certs and configure Nginx to offer both.

Why does my cert show as ‘not fully validated’ in some browsers?

Almost always a chain problem. The cert is fine, but the chain to a trusted root is incomplete. The right fix is to include the intermediate cert in fullchain.pem. The right verification is openssl s_client -connect example.com:443 -showcerts and look at the chain.

What if my server is behind a load balancer?

The load balancer terminates the SSL and forwards HTTP to the backend. The cert lives on the load balancer, not on the Nginx instance. The right answer is to either use a cloud-managed cert (ACM on AWS, Managed Certs on GCP) or to keep the cert on the load balancer and not have it on the backend at all.

How long is the Let’s Encrypt cert valid?

90 days. The auto-renewal runs twice a day, so the typical lead time is 30-60 days before expiry. The right answer is to set up the timer and the deploy hook and never think about it again.

Can I get a wildcard cert from Let’s Encrypt?

Yes. The certbot invocation is certbot certonly -d '*.example.com' -d example.com --dns-cloudflare (or similar with a DNS plugin). The challenge is DNS-01 instead of HTTP-01, so you need a DNS provider that certbot has a plugin for. The wrong answer is the HTTP-01 challenge for a wildcard, because the challenge requires the wildcard’s parent domain, which a HTTP-01 challenge cannot validate.

If you are sizing the infrastructure for the kind of project this post covers, the RunxBuild hosting calculator is the right place to model the line items. The compute, the memory, the storage, the bandwidth, the database - each one is a separate number, and the team’s mental model for the platform is the sum of those numbers. The RunxBuild dashboard is where the team sees the actual usage in one place.

Useful related references:

#ssl#install#dev-infra#tutorial