Skip to content
Let’s plan the right software for your processes. Call us for a demo or a quote: +90 546 737 48 29

TR EN DE

HTTPS in Nginx: setting up an SSL certificate

HTTPS encrypts the traffic between the visitor's browser and your server, and proves to the browser that it really is talking to your server. To do that, the server holds a certificate and the private key that belongs to it. Enabling HTTPS in Nginx comes down to three jobs: obtaining the certificate, telling Nginx where it is, and redirecting unencrypted requests to HTTPS.

This guide follows the general workflow with Let's Encrypt, which issues free certificates, and its widely used client, Certbot. If you bought your certificate from another provider, you can pick up from step three. If SSL and TLS are new to you, read the What is SSL? guide first.

In brief

  • Certbot obtains the certificate; Nginx is pointed at fullchain.pem and privkey.pem.
  • Every request arriving on port 80 is redirected to HTTPS with a 301.
  • HSTS is tried with a short lifetime first and extended once everything works.
  • Renewal must be automatic, and Nginx must be reloaded after each renewal.

What you need

  • Administrator (sudo) access to the server and a running Nginx
  • A domain name whose DNS record points to the server's IP address
  • Ports 80 and 443 reachable from the internet
  • A backup of the current Nginx configuration

Caution

Back up the configuration before changing it and test with "nginx -t" after every change. File paths and package names vary by distribution; adapt the examples to your own server.

On this page

HTTPS in seven steps

  1. Check the prerequisites

    Before issuing a certificate, Let's Encrypt verifies that the domain really is under your control. In the most common method, the validation server connects to your domain on port 80 and asks for a temporary file. So:

    • The DNS record of the domain (and of the www name, if you use it) must point to this server.
    • Ports 80 and 443 must be open in the firewall.
    • Nginx must have a server block for this domain with a correct server_name line.

    Where site files live varies by distribution: /etc/nginx/conf.d is common, while /etc/nginx/sites-available is the Debian/Ubuntu layout.

    Flow diagram: domain and ports, obtaining the certificate, HTTPS server block, redirect, testing, HSTS and automatic renewal : Enlarge
  2. Obtain the certificate with Certbot

    The command for installing Certbot depends on the distribution and package manager; the first line below is only an example. Once installed, the --nginx option obtains the certificate and adds the necessary lines to the matching server block itself. Each -d is one domain name.

    Bash
    # Installation varies by distribution. Example (Debian/Ubuntu package repository):
    sudo apt install certbot python3-certbot-nginx
    
    # Obtain the certificate and write it into the Nginx configuration
    sudo certbot --nginx -d example.com -d www.example.com

    Certbot places the files under /etc/letsencrypt/live/example.com/. Two of them matter to Nginx:

    • fullchain.pem: your site's certificate together with the intermediate certificate. ssl_certificate points to this file.
    • privkey.pem: the private key. ssl_certificate_key points to this file. It is never shared and never placed under the web root.
    Diagram: fullchain.pem contains the site certificate and the intermediate certificate and is referenced by ssl_certificate; privkey.pem is the private key and is referenced by ssl_certificate_key : Enlarge
  3. Write the HTTPS server block and the redirect

    If Certbot edited the configuration for you, simply review the result at this step. If you are writing it by hand, you need two blocks: one listens on port 80 and permanently (301) redirects every request to HTTPS, the other answers on port 443 with the certificate.

    Nginx
    # 80: redirect every request to HTTPS
    server {
        listen 80;
        listen [::]:80;
        server_name example.com www.example.com;
    
        return 301 https://$host$request_uri;
    }
    
    # 443: answer with the certificate
    server {
        listen 443 ssl;
        listen [::]:443 ssl;
        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_protocols TLSv1.2 TLSv1.3;
        ssl_session_cache shared:SSL:10m;
        ssl_session_timeout 1h;
    
        root /var/www/example.com/public;
        index index.html;
    }

    The line return 301 https://$host$request_uri; redirects while keeping the address and query string the visitor asked for. ssl_protocols TLSv1.2 TLSv1.3; switches off old, insecure protocol versions. The directive that enables HTTP/2 is written differently depending on the Nginx version; check the documentation for your version.

  4. Test and reload

    Test the syntax first; if there are no errors, reload Nginx. A reload does not drop open connections. Then check the redirect and the certificate from outside.

    Bash
    # Test the syntax, then reload
    sudo nginx -t
    sudo systemctl reload nginx
    
    # Does the HTTP address redirect to HTTPS?
    curl -sI http://example.com/ | grep -i -E "^(HTTP|location)"
    
    # HTTPS response and headers
    curl -sI https://example.com/
    
    # Show the certificate chain being served
    openssl s_client -connect example.com:443 -servername example.com < /dev/null

    The first curl output should show 301 and a location line starting with https://. In the browser, use the connection details in the address bar to check the certificate's domain name and expiry date. If something goes wrong, see "Common errors" below.

    Diagram: common errors in an HTTPS setup; missing intermediate certificate, mixed content, redirect loop, expired certificate : Enlarge
  5. If you proxy to a backend: SSL termination

    In most setups Nginx works as a reverse proxy: it receives the request and passes it on to the application behind it. In this arrangement the encrypted connection is opened at Nginx; this is called SSL termination (or TLS termination). The certificate lives in one place and the backend server does not have to deal with encryption.

    Nginx
    upstream app_backend {
        server 127.0.0.1:8080;
    }
    
    server {
        listen 443 ssl;
        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;
    
        location / {
            # Decrypted here; plain HTTP goes to the backend
            proxy_pass http://app_backend;
    
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            # The protocol the visitor used: https
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }

    There are two options for the connection between Nginx and the backend:

    • Plain HTTP: the usual choice when the backend is on the same server (127.0.0.1) or on a trusted, closed internal network.
    • Re-encryption: if the traffic crosses a network you do not trust, use proxy_pass https://…. By default Nginx does not verify the backend's certificate; verification needs proxy_ssl_verify on; and proxy_ssl_trusted_certificate.

    Because the backend sees the request as HTTP, it cannot tell that the visitor arrived over HTTPS. The X-Forwarded-Proto header carries that information; the application reads it to set secure cookies and build correct addresses. The application should trust this header only when it comes from your own proxy. More detail: Setting up a reverse proxy with Nginx.

    SSL termination diagram: the browser connects to Nginx over HTTPS, the traffic is decrypted at Nginx, and the request is passed to the backend over HTTP or re-encrypted, with the X-Forwarded-Proto header : Enlarge
  6. Enable HSTS with a short lifetime first

    The HSTS header (Strict-Transport-Security) tells the browser "connect to this site over HTTPS only for the stated period". It is strong protection but hard to undo: the browser will not go back to HTTP until the period runs out. So try it first with a max-age of a few minutes.

    Nginx
    # Trial: 5 minutes
    add_header Strict-Transport-Security "max-age=300" always;
    
    # If all is well, replace the line above with: 1 year
    # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    Define the header only in the block for port 443. The word always makes sure the header is sent with error responses too. Before adding includeSubDomains, make sure all your subdomains work over HTTPS. For the other headers, see the Security headers and HTTPS guide.

  7. Verify automatic renewal

    Let's Encrypt certificates are short-lived; they are not tracked by hand but renewed automatically. Certbot packages usually install a scheduler (a systemd timer or a cron job); it runs regularly and renews only certificates that are close to expiry. Confirm that the setup really works with a trial renewal.

    Bash
    # Trial renewal: does not touch the real certificate
    sudo certbot renew --dry-run
    
    # Certificates and expiry dates
    sudo certbot certificates
    
    # Is the timer installed? (on distributions that use systemd)
    systemctl list-timers | grep -i certbot
    
    # Reload Nginx after a certificate has been renewed
    sudo certbot renew --deploy-hook "systemctl reload nginx"

    Nginx reads the certificate file only when it starts and when it is reloaded. Without a reload after renewal, visitors are still served the old certificate even though the file has been renewed. The command given with --deploy-hook runs only when a certificate has actually been renewed. To make it permanent, you can also place the same command as an executable script in the /etc/letsencrypt/renewal-hooks/deploy/ directory; Certbot runs the scripts in that directory after every successful renewal.

    Flow diagram: the scheduler runs certbot renew, a certificate close to expiry is renewed, Nginx is reloaded, the new certificate is served : Enlarge

If you do not want Certbot to touch the configuration: the webroot method

If you prefer to manage the Nginx configuration entirely yourself, you can have Certbot obtain the certificate and nothing else. With this method the validation file is written to a directory of your choice, and the block for port 80 keeps that path out of the redirect.

Nginx
listen 80;
listen [::]:80;
server_name example.com www.example.com;

# Validation files are not redirected
location /.well-known/acme-challenge/ {
    root /var/www/letsencrypt;
}

location / {
    return 301 https://$host$request_uri;
}
Bash
# Obtain the certificate only; leave the Nginx configuration alone
sudo certbot certonly --webroot -w /var/www/letsencrypt -d example.com -d www.example.com

Wildcard certificates cannot be obtained this way; they require validation through a DNS record, and the steps depend on your DNS provider.

TLS settings: what to write and what to leave out

  • Protocol: ssl_protocols TLSv1.2 TLSv1.3; is enough for current browsers. For TLS 1.3, the OpenSSL library Nginx was built with must support that version.
  • Define it in one place: if the server hosts several sites, writing the protocol setting once in the http block prevents inconsistencies between sites.
  • Session cache: ssl_session_cache shared:SSL:10m; reduces repeated handshakes for returning visitors.
  • Cipher suites: do not copy old lists that circulate on the internet. Recommendations change over time; for current advice, see the documentation of your server software. If in doubt, keeping the default is better than pasting an outdated list.

Common errors and how to fix them

SymptomLikely causeFix
Works in the browser; certificate error on some phones, in apps or with curlMissing intermediate certificate: ssl_certificate points to the site certificate onlyPoint it to fullchain.pem
Warning on the padlock icon, some images or scripts do not loadMixed content: the page requests resources over http://Change the addresses in the pages and the database to https://
"Too many redirects" errorRedirect loop: the CDN or proxy in front connects to the server over HTTP, and the server redirects to HTTPS againSet the service in front to connect to the server over HTTPS, or make the redirect conditional
Certificate expired warningRenewal is not running, or Nginx was not reloaded after renewalRun a trial renewal; add the reload hook
The certificate cannot be issuedDNS points to another server, or port 80 is closedCheck the DNS record and the firewall
Name mismatch warningThe certificate does not cover the www name or the subdomainAdd the missing name with -d and obtain the certificate again

For a redirect loop: if a service in front of Nginx terminates TLS (a CDN such as Cloudflare, for example) and connects to the server over HTTP, you can make the redirect in the port 80 block depend on the protocol reported by that service:

Nginx
listen 80;
server_name example.com www.example.com;

# Redirect only if the visitor reached the service in front over HTTP
if ($http_x_forwarded_proto = "http") {
    return 301 https://$host$request_uri;
}

root /var/www/example.com/public;

Trust this header only if you are sure the requests really come from your own proxy. The lasting fix is to encrypt the connection between the service in front and your server as well. The same problem appears when the application behind Nginx redirects to HTTPS on its own but ignores the X-Forwarded-Proto header.

Checklist

  • ssl_certificate points to the full chain (fullchain.pem) and ssl_certificate_key to the private key.
  • The private key is readable only by the authorised user and is not exposed in backups or repositories.
  • The http:// address goes to the https:// address in a single step with a 301; there is no loop.
  • Only TLS 1.2 and 1.3 are enabled.
  • There are no mixed content warnings.
  • HSTS was tried with a short lifetime, then extended.
  • The trial renewal succeeds; Nginx is reloaded after renewal.
  • The certificate's expiry date is monitored separately: backup and monitoring.

Frequently asked questions

Is a Let's Encrypt certificate less secure than a paid one?

No. There is no difference in encryption, and browsers accept both in the same way. With paid certificates the difference lies in extras such as separate validation of the organisation's identity, support and warranty.

I renewed the certificate but the browser still shows the old one. Why?

Nginx reads the certificate file only when it starts and when it is reloaded. Test with "nginx -t", run "systemctl reload nginx", and add a reload hook to run after renewal.

Do I need separate certificates for the www and non-www addresses?

No. A single certificate can cover several names; pass each name to Certbot with its own -d option.

There is a CDN in front of my server. Do I still need a certificate on the server?

Yes, it is recommended. If the connection between the CDN and your server stays unencrypted, traffic is unprotected on that leg and problems such as redirect loops can appear. Set the CDN to connect to the server over HTTPS and to verify the certificate.

Can I undo HSTS after enabling it?

You can clear the record in browsers by sending the header with max-age=0, but that only takes effect in browsers that visit the site again, and only while HTTPS is working. This is why you should try a short lifetime before moving to a long one.

BYK Yazılım Support Team
This guide is written and regularly reviewed by the BYK Yazılım support team. Last updated: 4 October 2026.

Related guides

Let us talk about your website infrastructure

BYK Yazılım builds corporate websites. Write to us with any questions about your site.

Contact us Our corporate website service