HTTPS is a mandatory standard for modern websites. Nginx together with Let's Encrypt lets you configure free SSL in just a few minutes. This article covers the setup step by step: installing Certbot, configuring a server block, HSTS, OCSP stapling, choosing ciphers, and redirecting HTTP to HTTPS. You can check that SSL is configured correctly with the SSL Checker on reChecker. Ready-made configurations are generated with the Nginx Generator.
Installing Certbot
Certbot is the EFF's official client for obtaining Let's Encrypt certificates. It supports automatic Nginx configuration.
Ubuntu and Debian
sudo apt update
sudo apt install certbot python3-certbot-nginxThe python3-certbot-nginx plugin lets Certbot modify the Nginx configuration automatically.
CentOS, RHEL, Fedora
sudo dnf install certbot python3-certbot-nginxOr via EPEL for CentOS 7:
sudo yum install epel-release
sudo yum install certbot python3-certbot-nginxManual installation (snap)
Get the latest version via snap:
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbotFor Nginx without the plugin, use standalone or webroot mode.
Obtaining a certificate
Automatic mode (recommended)
Certbot reads the Nginx configuration and adds an SSL block:
sudo certbot --nginx -d example.com -d www.example.comFlags:
-d— domains for the certificate (you can specify several)--nginx— configure Nginx automatically--email your@email.com— email address for expiration notifications
Certbot will create or modify the server block, obtain the certificate, and configure an HTTP → HTTPS redirect.
Webroot mode (without stopping Nginx)
If Nginx is already serving the website and cannot be stopped:
sudo certbot certonly --webroot -w /var/www/example.com -d example.com -d www.example.comCertbot will place a file in /.well-known/acme-challenge/ to verify the domain. Make sure Nginx serves this directory:
location /.well-known/acme-challenge/ {
root /var/www/example.com;
allow all;
}Standalone mode
Certbot starts a temporary web server on port 80. Nginx must be stopped:
sudo systemctl stop nginx
sudo certbot certonly --standalone -d example.com
sudo systemctl start nginxCertificate file structure
Let's Encrypt stores certificates in /etc/letsencrypt/live/<domain>/:
| File | Purpose |
|---|---|
fullchain.pem | Certificate + CA chain (for Nginx) |
privkey.pem | Private key |
cert.pem | Domain certificate only |
chain.pem | Intermediate certificate chain |
The Nginx configuration uses fullchain.pem and privkey.pem.
Basic SSL server block
Minimal configuration:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
root /var/www/example.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}The http2 directive enables HTTP/2 (in Nginx 1.25.1+ use listen 443 ssl; with a separate http2 on;, depending on the version).
Redirecting HTTP to HTTPS
A separate server block for port 80:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}Or, preserve /.well-known to renew the certificate:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
location /.well-known/acme-challenge/ {
root /var/www/example.com;
allow all;
}
location / {
return 301 https://$host$request_uri;
}
}HSTS (HTTP Strict Transport Security)
HSTS tells the browser to always use HTTPS for the domain. The header is sent only over HTTPS:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;Parameters:
max-age=31536000— 1 yearincludeSubDomains— apply to subdomainspreload— allow the domain to be added to browsers' preload lists
Enable HSTS only after verifying that the entire website works over HTTPS. An error will cause the browser to reject HTTP for the specified period.
OCSP stapling
OCSP stapling allows the server to retrieve the certificate's revocation status and pass it to the client. It reduces latency and the load on the CA.
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;resolver — DNS servers for OCSP queries. For Let's Encrypt, chain.pem is suitable as a trusted certificate.
Cipher suites
Recommended configuration for TLS 1.2 and 1.3:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
# TLS 1.3 выбирает шифры сам, для 1.2 — явный список
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 — in TLS 1.3, the client sets the cipher order, so we disable the legacy behavior.
Modern minimum set (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:DHE-RSA-AES256-GCM-SHA384;Session cache and session resumption
Reduce full TLS handshakes by using a session cache:
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;shared:SSL:10m — a cache of 10 MB, shared across all worker processes. 1d — session lifetime of 1 day.
Complete SSL configuration
Combined example:
# HTTP → HTTPS
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
location /.well-known/acme-challenge/ {
root /var/www/example.com;
allow all;
}
location / {
return 301 https://$host$request_uri;
}
}
# HTTPS
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
root /var/www/example.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}Automatic certificate renewal
Let's Encrypt certificates are valid for 90 days. Certbot configures a cron job or systemd timer for automatic renewal.
Test renewal manually:
sudo certbot renew --dry-runPerform an actual renewal:
sudo certbot renewTypical cron job (twice a day):
0 0,12 * * * certbot renew --quiet --post-hook "systemctl reload nginx"--post-hook reloads Nginx after certificate renewal.
Wildcard certificates
Let's Encrypt issues wildcard certificates only through a DNS challenge:
sudo certbot certonly --manual --preferred-challenges dns -d example.com -d "*.example.com"Certbot will display a TXT record for _acme-challenge.example.com. Add it to DNS, wait for propagation, and press Enter. Automation is possible with plugins (Cloudflare, Route 53, etc.).
Multiple domains (SAN)
One certificate for multiple domains:
sudo certbot --nginx -d example.com -d www.example.com -d blog.example.com -d api.example.comAll domains will be included in one certificate (Subject Alternative Names).
SSL troubleshooting
Check the Nginx configuration:
sudo nginx -tCheck the certificate with OpenSSL:
openssl s_client -connect example.com:443 -servername example.comOnline check: reChecker SSL Checker — expiration date, chain, supported protocols.
Common errors
Certificate not found
The paths in ssl_certificate must point to existing files. Symlinks in /etc/letsencrypt/live/ point to the current versions in archive/.
OCSP stapling failed
Check resolver and verify that the OCSP server is reachable. For Let's Encrypt, chain.pem must be specified in ssl_trusted_certificate.
Mixed content
After switching to HTTPS, make sure all resources (scripts, stylesheets, images) are loaded over HTTPS. Otherwise, the browser blocks mixed content.
HTTP/2 is not working
Make sure Nginx was built with --with-http_v2_module. In newer versions, HTTP/2 is enabled via listen 443 ssl http2;.
Trusted root certificates
Nginx uses ssl_trusted_certificate for OCSP stapling and chain verification. The file must contain root and intermediate certificates. For Let's Encrypt, chain.pem is suitable. Custom CAs may require concatenating certificates:
cat /path/to/your-cert.pem /path/to/intermediate.pem /path/to/root.pem > fullchain.pemUsing HTTP/2 with SSL
HTTP/2 requires TLS (except h2c, which is rarely supported). In Nginx 1.25.1+, the syntax is:
listen 443 ssl http2;In older versions:
listen 443 ssl;
http2 on;Security tips
- Disable TLS 1.0 and 1.1 — keep only 1.2 and 1.3.
- Use strong ciphers — avoid RC4, 3DES, and MD5.
- Enable HSTS only after fully verifying HTTPS.
- Regularly update Nginx and OpenSSL.
Alternative Let's Encrypt clients
Besides Certbot, there are acme.sh, dehydrated, and lego. acme.sh is a bash script and does not require Python:
curl https://get.acme.sh | sh
~/.acme.sh/acme.sh --issue -d example.com -w /var/www/example.comThe Nginx configuration is set manually. Certbot with the Nginx plugin remains the simplest option for automatic configuration.
Related articles
For more on Nginx configuration, see the Nginx guide. For certificate types and choosing a CA, see the SSL certificates guide.