· 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.

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 nginxNginx starts automatically. Confirm it’s running:
systemctl status nginxVisit 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 reloadNginx Full opens both 80 (HTTP, needed for certificate validation) and 443 (HTTPS). Check it:
sudo ufw statusStep 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.comStart 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:
Hostpreserves the original domain so your app knows which site was requested.X-Real-IPandX-Forwarded-Forpass the real client IP — without these your app logs Nginx’s localhost address for every request.X-Forwarded-Prototells the app the original request was HTTPS, which frameworks use to build correct redirect URLs.- The
Upgrade/Connectionpair 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 nginxnginx -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 nginxAt 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-nginxRequest 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.comCertbot 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 nginxVisit 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 certbotDo a dry run to prove renewal actually works, without hitting the rate limits of a real renewal:
sudo certbot renew --dry-runIf 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.
Step 7: Harden the TLS Config (Optional but Recommended)
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 nginxDon’t add HSTS until HTTPS works reliably. Once a browser sees the
Strict-Transport-Securityheader it refuses plain HTTP for that domain for the fullmax-age— great for security, painful if your certificate setup is still broken.
Useful Nginx Commands
| Command | Description |
|---|---|
sudo nginx -t | Test config syntax before reloading |
sudo systemctl reload nginx | Apply config changes with no downtime |
sudo systemctl restart nginx | Full restart (drops connections) |
sudo tail -f /var/log/nginx/error.log | Watch errors live |
sudo certbot certificates | List installed certificates and expiry dates |
sudo certbot renew --dry-run | Test 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_pass — sudo 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:
- Installed Nginx and opened the firewall for HTTP and HTTPS
- Wrote a reverse proxy config forwarding to a local app with correct headers
- Enabled the site and tested the config safely
- Issued a free Let’s Encrypt certificate with Certbot
- Confirmed automatic renewal via the systemd timer
- 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



