Nginx as a reverse proxy with free HTTPS (Let's Encrypt)
Nginx sits in front of your app, handles HTTPS and passes requests along. Here is a small, safe configuration you can adapt.

A reverse proxy receives requests from the internet and forwards them to your app. Nginx is the common choice. Putting it in front of your Node, Python or other app gives you:
- HTTPS handled in one place.
- Static file serving and caching.
- Protection for the app: compression, size limits, rate limiting and a stable public port (80 and 443) while the app listens privately.
- The ability to run several sites on one server.
Step 1: run your app on a local port
Let your app listen only on 127.0.0.1 (for example port 3000), so it cannot be reached directly from outside.
Step 2: a minimal server block
Create /etc/nginx/sites-available/example and link it into sites-enabled.
server {
listen 80;
server_name example.com www.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
}
The proxy_set_header lines pass the original host, the visitor's IP and the protocol to your app. Without them, your app sees every request as coming from 127.0.0.1.
Test and reload:
sudo nginx -t
sudo systemctl reload nginx
nginx -t checks the syntax before you apply it. Always run it.
Step 3: free HTTPS with Certbot
Point your domain's DNS to the server first. Then:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
Certbot gets a free Let's Encrypt certificate, edits the server block to listen on 443 with TLS and can add a redirect from HTTP to HTTPS. Certificates last about 90 days and Certbot installs a timer to renew them automatically. Check with:
sudo certbot renew --dry-run
Step 4: sensible hardening
server_tokens off; # hide the Nginx version
client_max_body_size 10m; # limit upload size
add_header X-Content-Type-Options nosniff always;
add_header Strict-Transport-Security "max-age=31536000" always; # after HTTPS works everywhere
Also useful: gzip compression, long cache times for fingerprinted static files, and limit_req to rate limit expensive endpoints.
If a CDN is in front (such as Cloudflare)
Your server then sees the CDN's IP, not the visitor's. Use the real-IP module to trust the CDN's CF-Connecting-IP header only from the CDN's published IP ranges, and lock the origin firewall so that only the CDN can reach ports 80 and 443.
Common problems
- 502 Bad Gateway: the app is not running or listening on a different port. Check
systemctl statusand the port. - Redirect loops: the app and Nginx both redirect to HTTPS. Pass
X-Forwarded-Protoand let the app trust it. - WebSockets not working: add
proxy_set_header Upgrade $http_upgrade;andproxy_set_header Connection "upgrade";. - Certificate renewal fails: port 80 is blocked or the DNS changed.
Takeaway: keep the app on localhost, let Nginx terminate HTTPS with a free auto-renewing certificate, pass the proxy headers, always run nginx -t before reload, and add a few hardening headers.
Linux & DevOps series, Part 1 (servers and containers): 3 of 5. Previous: systemd: run your app as a service and schedule jobs with timers.


