What You'll End Up With
This nginx reverse proxy tutorial walks you through configuring Nginx on Ubuntu 24.04 to sit in front of one or more backend applications — Node.js, Python, Go, whatever — and forward requests to them over HTTP. By the end you'll have a working reverse proxy with proper headers, optional SSL termination via Certbot, and a systemd-managed Nginx service you can trust in production.
Prerequisites
- Ubuntu 24.04 LTS server (fresh or existing)
- A non-root user with
sudoprivileges - A backend process already listening on a local port (e.g.,
127.0.0.1:3000) - A domain name pointed at the server's IP (required for the SSL section)
- Ports 80 and 443 open in your firewall
Step 1: Install Nginx
Install Nginx from the Ubuntu package repository.
sudo apt update && sudo apt install -y nginx
Enable and start the service so it survives reboots.
sudo systemctl enable --now nginx
Verify the service is running.
sudo systemctl status nginx
Expected output (abbreviated):
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled)
Active: active (running)
Step 2: Create a Site Configuration File
Nginx on Ubuntu 24.04 reads per-site configs from /etc/nginx/sites-available/ and activates them via symlinks in /etc/nginx/sites-enabled/. Create a new config for your domain.
sudo nano /etc/nginx/sites-available/myapp
Paste the following block, replacing app.example.com with your domain and 3000 with your backend port.
server {
listen 80;
listen [::]:80;
server_name app.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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 60s;
proxy_connect_timeout 10s;
proxy_send_timeout 60s;
proxy_buffering on;
proxy_buffer_size 8k;
proxy_buffers 8 8k;
}
}
What each directive does:
proxy_pass— forwards requests to the backend address.proxy_http_version 1.1— required for WebSocket keepalive connections.X-Real-IP/X-Forwarded-For— passes the real client IP to your app.X-Forwarded-Proto— tells the app whether the original request was HTTP or HTTPS.Upgrade/Connection— enables WebSocket proxying.- Timeout values — sane defaults that prevent Nginx from hanging indefinitely.
Step 3: Enable the Site and Test the Configuration
Create a symlink to activate the site.
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myapp
Remove the default site if it's still enabled, to avoid a conflicting server_name _ catch-all.
sudo rm -f /etc/nginx/sites-enabled/default
Test the Nginx configuration for syntax errors.
sudo nginx -t
Expected output:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Reload Nginx to apply the new configuration without dropping active connections.
sudo systemctl reload nginx
Step 4: Configure SSL with Certbot
Skip this step if you're proxying internal traffic only. For any public-facing service, terminate TLS at Nginx.
Install Certbot and the Nginx plugin.
sudo apt install -y certbot python3-certbot-nginx
Obtain and install a certificate for your domain. Certbot will automatically edit your Nginx config to add the listen 443 ssl block and redirect HTTP to HTTPS.
sudo certbot --nginx -d app.example.com
Follow the prompts. When asked whether to redirect HTTP to HTTPS, choose option 2 (redirect).
Certbot installs a systemd timer that auto-renews certificates. Confirm it's active.
sudo systemctl status certbot.timer
Expected output:
● certbot.timer - Run certbot twice daily
Loaded: loaded (/usr/lib/systemd/system/certbot.timer; enabled)
Active: active (waiting)
After Certbot finishes, your config at /etc/nginx/sites-available/myapp will contain the SSL directives. Test and reload again.
sudo nginx -t && sudo systemctl reload nginx
Step 5: Proxy Multiple Backend Services
A single Nginx instance can proxy several apps on different subdomains or paths. Create a separate config file for each service, or use upstream blocks to define named backend pools.
Create a second site config for an API backend running on port 4000.
sudo nano /etc/nginx/sites-available/myapi
upstream api_backend {
server 127.0.0.1:4000;
keepalive 32;
}
server {
listen 80;
listen [::]:80;
server_name api.example.com;
location / {
proxy_pass http://api_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
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_read_timeout 60s;
proxy_connect_timeout 10s;
proxy_send_timeout 60s;
}
}
Why upstream with keepalive 32? It instructs Nginx to maintain a pool of persistent connections to the backend, reducing TCP handshake overhead on high-traffic APIs.
Enable and reload.
sudo ln -s /etc/nginx/sites-available/myapi /etc/nginx/sites-enabled/myapi
sudo nginx -t && sudo systemctl reload nginx
Verify It Works
Run each check in order.
1. Confirm Nginx is listening on the expected ports.
sudo ss -tlnp | grep nginx
Expected output:
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",...)
LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("nginx",...)
2. Send a test request through the proxy.
curl -I http://app.example.com
If SSL is configured, test HTTPS.
curl -I https://app.example.com
Expected output includes HTTP/2 200 (or HTTP/1.1 200) and the response headers from your backend.
3. Confirm the forwarded IP header reaches your app.
On your backend, log the X-Forwarded-For header and compare it against your client's public IP.
4. Check the Nginx access log in real time.
sudo tail -f /var/log/nginx/access.log
Make a request from a browser and watch the log line appear with your IP, status code, and upstream response time.
Troubleshooting
502 Bad Gateway Nginx reached the backend address but got no response. Confirm your backend process is running and listening on the correct port.
sudo ss -tlnp | grep 3000
If the port is empty, your backend crashed. Check its logs.
504 Gateway Timeout
The backend is running but not responding within the timeout window. Increase proxy_read_timeout in the location block, or investigate why the backend is slow.
nginx -t fails with "conflicting server name"
Two config files declare the same server_name. Check /etc/nginx/sites-enabled/ for duplicates and remove the conflicting file.
ls -la /etc/nginx/sites-enabled/
Certificate renewal fails Run a dry-run to diagnose.
sudo certbot renew --dry-run
Common causes: port 80 blocked by a firewall rule, or the server_name in the Nginx config doesn't match the certificate domain.
"connect() failed (111: Connection refused)" in error log
Nginx can't reach 127.0.0.1:<port>. Verify the backend is bound to 127.0.0.1 and not 0.0.0.0 on a different interface, and that no iptables rule is blocking loopback traffic.
sudo tail -50 /var/log/nginx/error.log
Client IP shows 127.0.0.1 in app logs instead of real IP
Your backend is reading the wrong header. Configure it to trust the X-Forwarded-For header. For Express.js: set app.set('trust proxy', 1). For Django: add SECURE_PROXY_SSL_HEADER and use django-ipware.
Next Steps
With the nginx reverse proxy tutorial complete, consider these additions for a production-hardened setup:
- Rate limiting — Add
limit_req_zoneandlimit_reqdirectives to protect against abusive clients. - Gzip compression — Enable
gzip onin/etc/nginx/nginx.confto reduce bandwidth on text responses. - Security headers — Add
X-Content-Type-Options,X-Frame-Options, andStrict-Transport-Securityin aadd_headerblock. - Log rotation — Ubuntu's default
logrotateconfig at/etc/logrotate.d/nginxrotates logs weekly; adjust to daily if traffic is high. - Monitoring — Enable the
ngx_http_stub_status_moduleand scrape metrics with Prometheus and Grafana setup tutorial for visibility into active connections and request rates.
All of the above can run comfortably on a €4/month Hetzner CAX11 instance — no need to over-provision.