· Sysadmin.id · Linux · 6 min read

Nginx Reverse Proxy with Free Let's Encrypt SSL on Ubuntu

Put Nginx in front of your app and serve it over HTTPS — a complete guide to reverse proxying, free Let's Encrypt certificates, auto-renewal, and a hardened TLS config on Ubuntu 24.04.

Put Nginx in front of your app and serve it over HTTPS — a complete guide to reverse proxying, free Let's Encrypt certificates, auto-renewal, and a hardened TLS config on Ubuntu 24.04.

Most apps — a Node service, a Python API, a Docker container — listen on some local port like 3000 and speak plain HTTP. You don’t expose that to the internet directly. You put Nginx in front of it.

A reverse proxy gives you one clean entry point: it terminates TLS, serves HTTPS on port 443, and forwards requests to your app over localhost. Your app stays simple and private; Nginx handles the public-facing concerns.

This guide sets up an Nginx reverse proxy on Ubuntu 24.04, fronting an app on 127.0.0.1:3000, with a free Let’s Encrypt certificate and automatic renewal.

Prerequisites

Before you start, make sure you have:

  • A server running Ubuntu 24.04 LTS with a public IP
  • Root or sudo access
  • A domain name with an A record pointing to your server’s IP (e.g. app.example.com)
  • An app already running and listening locally — this guide assumes 127.0.0.1:3000

DNS must resolve first. Let’s Encrypt validates by reaching your domain over HTTP. Confirm with dig +short app.example.com — it should return your server’s IP before you continue.


Step 1: Install Nginx

sudo apt update
sudo apt install -y nginx

Nginx starts automatically. Confirm it’s running:

systemctl status nginx

Visit http://your-server-ip in a browser — you’ll see the default Nginx welcome page. That confirms it’s serving.


Step 2: Allow HTTP and HTTPS Through the Firewall

If UFW is active, open the web ports. Nginx registers handy UFW profiles on install:

sudo ufw allow 'Nginx Full'
sudo ufw reload

Nginx Full opens both 80 (HTTP, needed for certificate validation) and 443 (HTTPS). Check it:

sudo ufw status

Step 3: Create the Reverse Proxy Config

Nginx server configs live in /etc/nginx/sites-available/ and are activated by symlinking into sites-enabled/. Create a config for your domain:

sudo nano /etc/nginx/sites-available/app.example.com

Start with a plain HTTP server block that proxies to your app. Certbot will add the TLS parts in Step 5.

server {
    listen 80;
    listen [::]:80;
    server_name app.example.com;

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

        # Pass real client info to the app
        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;

        # Support WebSockets (Socket.IO, live reload, etc.)
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

The headers matter:

  • Host preserves the original domain so your app knows which site was requested.
  • X-Real-IP and X-Forwarded-For pass the real client IP — without these your app logs Nginx’s localhost address for every request.
  • X-Forwarded-Proto tells the app the original request was HTTPS, which frameworks use to build correct redirect URLs.
  • The Upgrade/Connection pair lets WebSocket connections pass through the proxy.

Step 4: Enable the Site

Symlink the config into sites-enabled, test the syntax, and reload:

sudo ln -s /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/

# Always test before reloading — catches typos before they take the site down
sudo nginx -t

sudo systemctl reload nginx

nginx -t should report syntax is ok and test is successful. If you no longer want the default welcome site answering for unmatched domains, remove its symlink:

sudo rm /etc/nginx/sites-enabled/default
sudo systemctl reload nginx

At this point http://app.example.com proxies to your app over plain HTTP. Now add TLS.


Step 5: Get a Free Let’s Encrypt Certificate

Certbot is the official Let’s Encrypt client. Install it with the Nginx plugin:

sudo apt install -y certbot python3-certbot-nginx

Request a certificate. The --nginx plugin reads your server block, proves you control the domain, installs the certificate, and rewrites your config to serve HTTPS — all in one command:

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

Certbot asks for an email (for expiry warnings) and whether to redirect HTTP to HTTPS — choose redirect. When it finishes, it has edited your config to add a listen 443 ssl block and an automatic redirect from port 80.

Reload to be sure the new config is live:

sudo systemctl reload nginx

Visit https://app.example.com — you’ll see the padlock, and your app served securely.

Multiple domains? Pass each with its own -d: sudo certbot --nginx -d example.com -d www.example.com. One certificate can cover several names.


Step 6: Verify Automatic Renewal

Let’s Encrypt certificates last 90 days. Certbot installs a systemd timer that renews them automatically — you don’t need a cron job. Confirm the timer is active:

systemctl list-timers | grep certbot

Do a dry run to prove renewal actually works, without hitting the rate limits of a real renewal:

sudo certbot renew --dry-run

If it reports success, your certificates will renew silently in the background for as long as the server runs. Certbot reloads Nginx for you after each renewal.


Certbot’s defaults are reasonable, but you can tighten things. Add a few directives inside your 443 server block (or in a shared snippet) for stronger security headers:

# Force HTTPS for a year, including subdomains
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

# Stop the browser from guessing content types
add_header X-Content-Type-Options "nosniff" always;

# Limit how much referrer info leaks to other sites
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

Test and reload after any change:

sudo nginx -t && sudo systemctl reload nginx

Don’t add HSTS until HTTPS works reliably. Once a browser sees the Strict-Transport-Security header it refuses plain HTTP for that domain for the full max-age — great for security, painful if your certificate setup is still broken.


Useful Nginx Commands

CommandDescription
sudo nginx -tTest config syntax before reloading
sudo systemctl reload nginxApply config changes with no downtime
sudo systemctl restart nginxFull restart (drops connections)
sudo tail -f /var/log/nginx/error.logWatch errors live
sudo certbot certificatesList installed certificates and expiry dates
sudo certbot renew --dry-runTest renewal without using rate limits

Common Issues and Fixes

502 Bad Gateway

Nginx is up but can’t reach your app. Confirm the app is actually listening on the port in proxy_passsudo ss -tlnp | grep 3000. If the app binds to 0.0.0.0:3000 or 127.0.0.1:3000 you’re fine; if it binds to a Docker-internal address, point proxy_pass at the right host.

Certbot fails with “challenge failed”

DNS or firewall. The domain’s A record must point to this server, and port 80 must be open — Let’s Encrypt validates over HTTP first. Re-check dig +short app.example.com and sudo ufw status.

Redirect loop after enabling HTTPS

Usually an app behind the proxy that doesn’t trust X-Forwarded-Proto and keeps redirecting to HTTPS itself. Make sure the header is set (Step 3) and that your framework is configured to trust the proxy.

WebSocket connections drop

The Upgrade and Connection "upgrade" headers must be present in the location block. Without them Nginx closes the connection on protocol switch.

Config change not taking effect

You edited sites-available but the symlink in sites-enabled points elsewhere, or you forgot to reload. Run sudo nginx -t then sudo systemctl reload nginx.


Summary

Here’s what you built:

  1. Installed Nginx and opened the firewall for HTTP and HTTPS
  2. Wrote a reverse proxy config forwarding to a local app with correct headers
  3. Enabled the site and tested the config safely
  4. Issued a free Let’s Encrypt certificate with Certbot
  5. Confirmed automatic renewal via the systemd timer
  6. Hardened the TLS config with security headers

Your app now sits safely behind Nginx, served over HTTPS with a certificate that renews itself. Adding another app is just another server block and one more certbot run.

Need a reverse proxy for a complex stack, load balancing across multiple backends, or a full server hardening pass? Get in touch — I’m happy to help.

  • nginx
  • ssl
  • lets-encrypt
  • reverse-proxy
  • ubuntu
  • security
Share:

Written by

tox — Sysadmin & DevOps freelancer

I keep Linux servers, cloud infrastructure, and deployments running for startups and developers worldwide — the same work this guide covers. 99% job success across 100+ Upwork contracts, 10,000+ hours.

Back to Blog

Related Posts

View All Posts »
How to Install Docker on Ubuntu 24.04 LTS

How to Install Docker on Ubuntu 24.04 LTS

A complete step-by-step guide to installing Docker Engine on Ubuntu 24.04 LTS (Noble Numbat) — from adding the official repository to running your first container and setting up Docker Compose.