How to set up an Nginx reverse proxy on Ubuntu

An Nginx reverse proxy takes requests on ports 80 and 443 and passes them to your app, which listens only on the server itself. This is the usual way to publish a Node.js, Python or Go app: Nginx handles HTTPS, slow clients and static files, the app handles the logic. The guide is for Ubuntu 24.04 with Nginx 1.24 and works the same on 22.04.

How to proxy a domain to an app on port 3000

Start the app so that it listens on 127.0.0.1:3000, not on all addresses: then nobody reaches it from outside past Nginx. In Node.js that is server.listen(3000, '127.0.0.1'), in Gunicorn or Uvicorn --bind 127.0.0.1:3000 or --host 127.0.0.1 --port 3000. If Nginx isn’t installed yet, see how to install Nginx on Ubuntu.

Create a site file. Replace app.example.com with your domain:

sudo nano /etc/nginx/sites-available/app.example.com
/etc/nginx/sites-available/app.example.com
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; } }

Turn the site on, check the configuration, reload Nginx and ask it for the page:

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/

The answer should carry your app’s status and headers with Server: nginx on top. Write 127.0.0.1 in proxy_pass rather than localhost: localhost also resolves to the IPv6 address [::1], and an app that listens only on IPv4 refuses those connections.

What the proxy_set_header lines do

Without them the app sees every request as coming from 127.0.0.1 and doesn’t know the real domain or whether the visitor used HTTPS.

  • Host $host: the domain the visitor typed. By default Nginx sends the address from proxy_pass instead, and links built by the app point to 127.0.0.1.
  • X-Real-IP $remote_addr: the visitor’s IP address.
  • X-Forwarded-For $proxy_add_x_forwarded_for: the chain of addresses the request passed, with the visitor’s address added at the end.
  • X-Forwarded-Proto $scheme: http or https. Apps use it to build correct links and to set secure cookies after you add HTTPS.

The app must trust these headers only from the proxy. Most frameworks have a setting for it, such as trust proxy in Express or --forwarded-allow-ips in Gunicorn and Uvicorn. Ubuntu’s Nginx package also ships /etc/nginx/proxy_params with the same four headers, so include proxy_params; can replace them.

How to pass WebSocket connections

A WebSocket starts as an ordinary request with the Upgrade header, which Nginx doesn’t forward by itself. Nginx 1.24 also talks to the app over HTTP/1.0 unless told otherwise, so keep proxy_http_version 1.1. First add a map that sets the Connection header. It must live inside the http block, and Ubuntu reads every file in /etc/nginx/conf.d/ there:

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

Then add two lines to location / in the site file. The whole block now looks like this:

/etc/nginx/sites-available/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 $connection_upgrade; }
sudo nginx -t sudo systemctl reload nginx

Nginx closes a proxied connection that sends nothing for 60 seconds (proxy_read_timeout). For a chat or live updates, let the app send WebSocket pings more often, or raise proxy_read_timeout in this location.

Timeouts and the “413 Request Entity Too Large” error

Nginx accepts request bodies up to 1 MB by default. A bigger upload gets 413 Request Entity Too Large, and error.log shows client intended to send too large body. Raise client_max_body_size in the server block to the largest file you expect. The three proxy timeouts are 60 seconds each by default and count the gap between two reads or writes, not the whole request. Raise proxy_read_timeout only if the app really needs more time to answer, for example for a report or an export.

/etc/nginx/sites-available/app.example.com
client_max_body_size 50m; location / { # the proxy_pass and proxy_set_header lines from above proxy_connect_timeout 5s; proxy_send_timeout 60s; proxy_read_timeout 120s; }
sudo nginx -t sudo systemctl reload nginx

A short proxy_connect_timeout suits an app on the same server: if it can’t connect in 5 seconds, the app is down, and the visitor gets an error at once instead of after a minute. If the app is in PHP, its own upload limits apply too: how to raise the PHP memory and upload limits.

Why Nginx returns 502 Bad Gateway

A 502 means Nginx reached no answer from the app. The reason is always in the error log, in the line next to the request:

sudo tail -n 20 /var/log/nginx/error.log
  • connect() failed (111: Connection refused) while connecting to upstream: nothing listens on that port. Check that the app runs and on which address: sudo ss -tlnp | grep 3000 If the log shows upstream: "http://[::1]:3000/", write 127.0.0.1 in proxy_pass instead of localhost.
  • upstream prematurely closed connection while reading response header from upstream: the app crashed or closed the connection while answering. Look at the app’s own log, for example: journalctl -u <your app service> -n 50
  • upstream sent too big header while reading response header from upstream: the app sends headers bigger than the buffer, usually many or large cookies. Add proxy_buffer_size 16k; and proxy_buffers 8 16k; to the location.
  • upstream timed out (110: Connection timed out) while reading response header from upstream: this one gives 504 Gateway Time-out, not 502. The app takes longer than proxy_read_timeout. Speed up the slow request or raise the timeout for it.

If you also work on a Mac

MacWashing, System Junk: Xcode junk, user caches, installers and logs found, 22.83 GB ready to be cleaned

System Junk in MacWashing finds the caches of every app it knows, including those hidden inside app data, plus logs, Xcode junk and leftovers of deleted apps. It shows how much each takes and clears them in one go. Anything debatable stays unchecked until you decide.

Free trial · macOS 14 or later

Questions and answers

What is an Nginx reverse proxy?

It is Nginx standing in front of your app: visitors connect to Nginx on ports 80 and 443, and Nginx passes each request to the app on a local port and sends the answer back. The app stays closed to the outside world.

How do I add HTTPS to the proxy?

Point the domain to the server and run Certbot with the Nginx plugin. It adds the certificate to the same site file and keeps proxy_pass as it is: how to get a Let’s Encrypt certificate with Certbot.

Can one Nginx proxy several apps?

Yes. Give each app its own port and its own site file with its own server_name, or one domain with several location blocks, each with its own proxy_pass.