Skip to content

TutorialsWeb servers

How to set up Nginx as a reverse proxy with Let’s Encrypt and Certbot

Put apps behind Nginx on Ubuntu or Debian with WebSocket support and forwarded headers, then add free Let’s Encrypt HTTPS with Certbot and automatic renewal.

  • Intermediate
  • 35 min read
  • Updated

Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13

This guide is not available in your language yet, so it is shown in English.

On this page
  1. Prerequisites
  2. Step 1 — Install Nginx
  3. Step 2 — Open the firewall
  4. Step 3 — Add a WebSocket helper map
  5. Step 4 — Create a server block for your app
  6. Step 5 — Install Certbot
  7. Step 6 — Get a certificate with the nginx plugin
  8. Step 7 — Check automatic renewal
  9. Step 8 — Harden the HTTPS site
  10. Back up and restore
  11. Update Nginx and Certbot
  12. Troubleshooting
  13. 502 Bad Gateway
  14. 413 Request Entity Too Large
  15. WebSockets fail or disconnect after about a minute
  16. Could not automatically find a matching server block
  17. Certbot fails with Timeout during connect or a DNS problem
  18. Redirect loop or the app generates http:// links
  19. Next steps

Nginx is a fast, widely used web server that many administrators already know. As a reverse proxy it sits on ports 80 and 443 and forwards requests to apps listening on local ports, for example a Docker container published on 127.0.0.1:8080. Certbot, the Let's Encrypt client maintained by the EFF, obtains a free certificate, edits the Nginx configuration to use it and renews it automatically. This guide installs Nginx from your distribution's repository, writes a reverse proxy server block with WebSocket support and the usual forwarded headers, adds HTTPS with the Certbot snap, verifies renewal, and covers hardening, backups, updates and common errors.

Prerequisites

  • A server running Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12 or Debian 13.
  • A non-root user with sudo rights and SSH key login: Secure a new Linux server and Set up SSH keys.
  • A domain name with an A record (and an AAAA record if the server has working IPv6) pointing at the server for each hostname, such as app.example.com. Let's Encrypt prefers IPv6 when an AAAA record exists, so remove AAAA records the server cannot answer on.
  • An app listening on a local port. For Docker apps, publish ports on 127.0.0.1 only: see Docker Compose basics.
  • Ports 80 and 443 not used by another web server.

Neither Nginx nor Certbot publishes minimum hardware requirements. The values below are a conservative starting point for the proxy alone; size the server for the apps behind it.

ResourceMinimum (official)Suggested starting point
CPUNot published1 vCPU, shared with your apps
RAMNot published256 MB free for Nginx and Certbot
DiskNot published1 GB free for logs, snaps and certificates

Step 1 — Install Nginx

This guide uses the nginx package from Ubuntu or Debian. It receives security fixes through your distribution's security updates (and unattended-upgrades on Ubuntu), uses the sites-available and sites-enabled layout that Certbot's nginx plugin handles well, and ships ufw application profiles. The versions are older than upstream (for example 1.24 on Ubuntu 24.04 and 1.28 on Ubuntu 26.04), but they have every feature a reverse proxy needs.

Bash
sudo apt update
sudo apt install nginx
systemctl status nginx --no-pager
curl -I http://localhost

The service starts automatically, and curl returns HTTP/1.1 200 OK from the default welcome page.

Step 2 — Open the firewall

The distribution package installs ufw profiles for Nginx. Nginx Full opens ports 80 and 443:

Bash
sudo ufw app list
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status verbose

On Debian, install ufw first with sudo apt install ufw. If ufw app list shows no Nginx profiles, allow 80/tcp and 443/tcp instead. Keep port 80 open after HTTPS works: Let's Encrypt uses it for the HTTP-01 challenge at every renewal.

Step 3 — Add a WebSocket helper map

Many self-hosted apps use WebSockets for live updates. To proxy them, Nginx must forward the Upgrade header and set Connection to upgrade only when the client asked for an upgrade. A map in the http context does that once for all sites. Ubuntu's and Debian's nginx.conf include every file in /etc/nginx/conf.d/, so create one there:

Bash
sudo nano /etc/nginx/conf.d/websocket-upgrade.conf
Nginx
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

Step 4 — Create a server block for your app

Create a configuration file named after the hostname:

Bash
sudo nano /etc/nginx/sites-available/app.example.com
Nginx
server {
    listen 80;
    listen [::]:80;
    server_name app.example.com;

    # largest upload the app accepts; the Nginx default is 1m
    client_max_body_size 50m;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_read_timeout 300s;
    }
}

What the directives do:

  • server_name must match the hostname exactly; Certbot uses it to find the block.
  • proxy_pass without a path forwards the request URI unchanged to the app.
  • proxy_http_version 1.1 is required for WebSockets on Nginx versions before 1.29.7, which includes every distribution package listed above. Newer versions use 1.1 by default.
  • Host passes the original hostname instead of 127.0.0.1. The X-Forwarded-* and X-Real-IP headers tell the app the visitor's address and that the original request used HTTPS.
  • client_max_body_size raises the upload limit. With the default of 1 MB, larger uploads fail with 413 Request Entity Too Large.
  • proxy_read_timeout keeps idle WebSocket and long-polling connections open for 5 minutes instead of the default 60 seconds.

Enable the site, test the syntax and reload:

Bash
sudo ln -s /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
curl -I http://app.example.com

nginx -t must report syntax is ok and test is successful. The curl request should return your app's response over plain HTTP. For each further app, repeat this step with a new file, hostname and port.

Step 5 — Install Certbot

The Certbot team recommends installing Certbot as a snap. Ubuntu Server includes snapd. On Debian, install it first, then log out and back in (or reboot) so the snap paths are set:

Bash
sudo apt update
sudo apt install snapd

Remove any Certbot package from apt so that the certbot command runs the snap, then install Certbot and link the command:

Bash
sudo apt remove certbot
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/local/bin/certbot
certbot --version

If Certbot was never installed with apt, the first command just reports that the package is not installed.

Step 6 — Get a certificate with the nginx plugin

Request a certificate for the hostname and let Certbot update the server block:

Bash
sudo certbot --nginx -d app.example.com

The first run asks for an email address and for your agreement to the Let's Encrypt terms. Certbot then proves control of the domain over port 80, saves the certificate under /etc/letsencrypt/live/app.example.com/, adds listen 443 ssl and the certificate paths to your server block, and creates a separate port 80 block that redirects all HTTP requests to HTTPS. To cover several hostnames in one certificate, repeat -d, for example -d example.com -d www.example.com.

Verify the result:

Bash
sudo certbot certificates
curl -I http://app.example.com
curl -I https://app.example.com

certbot certificates lists the domains, the expiry date and the file paths. The HTTP request returns 301 Moved Permanently to the HTTPS address, and the HTTPS request returns your app's response.

Step 7 — Check automatic renewal

The Certbot snap installs a systemd timer that runs certbot renew regularly. Each run renews certificates that are close to expiry; since Certbot 4.0, that means less than a third of their lifetime is left. Check the timer and simulate a renewal against the Let's Encrypt staging environment:

Bash
systemctl list-timers | grep certbot
sudo certbot renew --dry-run

The dry run ends with Congratulations, all simulated renewals succeeded. Because the certificate was installed with the nginx plugin, Certbot reloads Nginx after every real renewal.

Automatic renewal matters more every year. Let's Encrypt certificates are valid for 90 days today; Let's Encrypt has announced 64-day certificates from February 2027 and 45-day certificates from February 2028. It also stopped sending expiry reminder emails in June 2025, so check sudo certbot certificates now and then or use an external certificate monitor.

Step 8 — Harden the HTTPS site

A few small changes make the setup tighter:

  • Hide the version number. In /etc/nginx/nginx.conf, uncomment server_tokens off; in the http block.
  • Turn on HTTP/2. On Nginx 1.25.1 and newer (Ubuntu 26.04, Debian 13), add http2 on; to the HTTPS server block. On Ubuntu 24.04 and Debian 12, add http2 after ssl on the listen 443 ssl lines that Certbot wrote (IPv4 and IPv6) instead. Check your version with nginx -v.
  • Consider HSTS carefully. HSTS tells browsers to use HTTPS only for this hostname for a set time. Start with a short lifetime inside the HTTPS server block and raise it only after everything works:
Nginx
add_header Strict-Transport-Security "max-age=300" always;

After each change, run sudo nginx -t and sudo systemctl reload nginx.

Back up and restore

Two directories hold the whole setup:

  • /etc/nginx/ — nginx.conf, conf.d/ and all server blocks,
  • /etc/letsencrypt/ — certificates, private keys, the ACME account and the renewal configuration.

Archive both, keeping the symbolic links inside /etc/letsencrypt/live/ intact:

Bash
sudo mkdir -p /opt/backups
sudo tar czf /opt/backups/nginx-letsencrypt-$(date +%F).tar.gz /etc/nginx /etc/letsencrypt

To restore on a new server, install Nginx and Certbot as in Steps 1 and 5, unpack the archive, then test the configuration and renewal:

Bash
sudo tar xzf /opt/backups/nginx-letsencrypt-2026-10-09.tar.gz -C /
sudo nginx -t
sudo systemctl reload nginx
sudo certbot renew --dry-run

The archive contains private keys, so keep it with restricted permissions and copy it off the server.

Update Nginx and Certbot

Nginx updates come with your normal system updates. Read the changelog of your distribution package for anything that touches your configuration, then upgrade:

Bash
sudo apt update
sudo apt upgrade
nginx -v
sudo nginx -t

Snaps refresh automatically in the background, so Certbot stays current. Check the installed version and force a refresh if needed:

Bash
snap list certbot
sudo snap refresh certbot

Troubleshooting

502 Bad Gateway

Nginx cannot reach the app. Look in /var/log/nginx/error.log for connect() failed (111: Connection refused) while connecting to upstream, then test the app directly with curl -I http://127.0.0.1:8080. Check that the container or service runs and that the port in proxy_pass matches the published port.

413 Request Entity Too Large

The upload is larger than client_max_body_size. Raise the value in the server block, for example to 100m, then test and reload Nginx. Some apps have their own upload limit as well.

WebSockets fail or disconnect after about a minute

Check that the map from Step 3 exists, that the location sends both the Upgrade and Connection headers, and that proxy_http_version 1.1 is set. Idle connections are closed after proxy_read_timeout, 60 seconds by default; raise it or let the app send WebSocket pings.

Could not automatically find a matching server block

Certbot found no server block whose server_name matches the -d hostname. Fix the server_name line, make sure the site is linked into sites-enabled, run sudo nginx -t and sudo systemctl reload nginx, then run Certbot again.

Certbot fails with Timeout during connect or a DNS problem

Let's Encrypt could not reach http://app.example.com/.well-known/acme-challenge/ from the internet. Check the A and AAAA records with dig +short, the ufw rules, any external firewall, and that port 80 is served by Nginx. Repeated failures count against the rate limit of 5 failed validations per hostname per hour, so fix the cause before retrying, or test with --dry-run first.

The app does not know the original request used HTTPS. Make sure X-Forwarded-Proto is set as in Step 4 and configure the app to trust the proxy at 127.0.0.1. If a CDN sits in front of the server, set its TLS mode so that it connects to the server over HTTPS, not HTTP.

Next steps

Frequently asked questions

Should I install Certbot with snap or apt?

The Certbot team recommends the snap for most users, because it always carries the current Certbot release and renews itself. Ubuntu and Debian also package Certbot, but distribution packages tend to fall behind on LTS releases.

Do I need to reload Nginx after Certbot renews a certificate?

No. When the certificate was installed with the nginx plugin, Certbot reloads Nginx after each successful renewal. Use a deploy hook only if other services also read the certificate files.

Why does my app still see 127.0.0.1 as the client address?

Nginx passes the visitor address in the X-Real-IP and X-Forwarded-For headers. The app must be told to trust the proxy at 127.0.0.1 and read those headers; most frameworks and self-hosted apps have a trusted proxy setting for this.

Can I get a wildcard certificate with certbot --nginx?

No. Wildcard certificates require the DNS-01 challenge, which needs a DNS plugin or manual TXT records. The nginx plugin uses the HTTP-01 challenge, so request one certificate per hostname or list several names with -d.

Will Let’s Encrypt email me before a certificate expires?

No. Let’s Encrypt stopped sending expiration emails in June 2025. Rely on the renewal timer, test it with certbot renew --dry-run and monitor your certificates yourself.

Sources

Generiraj lozinku

Please confirm