An nginx reverse proxy config can look intimidating when you are still learning:

server {
    listen 80;
    server_name app.example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        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 config is not random. It is just a set of instructions that says:

When someone visits this domain, send the request to my app running somewhere else, then return the app’s response to the visitor.

The nginx Reverse Proxy Wizard helps build the config step by step so I do not have to remember every directive from scratch.

What a reverse proxy does

A reverse proxy sits in front of an app.

The visitor talks to nginx:

browser -> nginx

nginx talks to the app:

nginx -> app

Then nginx sends the app’s response back to the visitor.

This is useful because nginx can handle the public-facing parts:

  • domain name;
  • TLS/HTTPS;
  • static files;
  • request size limits;
  • proxy headers;
  • multiple apps on one server.

Your app can stay on a private local port, such as 127.0.0.1:3000.

Step 1: know your public domain

Start with the domain people will open in the browser.

Example:

app.example.com

In nginx, this becomes:

server_name app.example.com;

The domain should already point to your server’s IP address with DNS. nginx cannot fix DNS. It can only respond after traffic reaches the server.

Step 2: know where your app is running

Next, identify the app address from the server’s point of view.

Common examples:

http://127.0.0.1:3000
http://localhost:8080
http://172.18.0.5:5000
http://app:3000

If the app runs directly on the same server, 127.0.0.1:3000 is common.

If the app runs in Docker Compose and nginx is in the same Compose network, the upstream might be a service name like:

http://app:3000

The important question is:

Can nginx reach this address from where nginx is running?

Step 3: choose the basic reverse proxy template

Open the nginx Reverse Proxy Wizard. Fill in:

  1. the domain name;
  2. the upstream app URL;
  3. whether TLS is handled by nginx;
  4. any extra options you need.

A minimal generated config usually has:

location / {
    proxy_pass http://127.0.0.1:3000;
}

That line means:

Send requests under / to the app at http://127.0.0.1:3000.

Step 4: keep the forwarding headers

A reverse proxy changes the path a request takes. Without extra headers, the app may not know the original host, IP, or protocol.

These headers are commonly used:

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;

Plain meaning:

  • Host: the domain the visitor requested;
  • X-Real-IP: the direct client IP seen by nginx;
  • X-Forwarded-For: the chain of client/proxy IPs;
  • X-Forwarded-Proto: whether the original request used http or https.

Many apps need these to build correct redirects, logs, and absolute URLs.

Step 5: understand HTTP vs HTTPS

There are two common setups.

First setup: nginx handles HTTPS directly.

browser --HTTPS--> nginx --HTTP--> app

This is common. The app only listens locally over HTTP. nginx handles certificates.

Second setup: a load balancer or CDN handles HTTPS before nginx.

browser --HTTPS--> CDN/load balancer --HTTP or HTTPS--> nginx --HTTP--> app

In that case, make sure nginx and the app understand the forwarded protocol headers. Otherwise the app may think the request is HTTP and generate wrong redirects.

Step 6: decide whether to redirect HTTP to HTTPS

For public sites, I usually want HTTP to redirect to HTTPS.

That looks like:

server {
    listen 80;
    server_name app.example.com;
    return 301 https://$host$request_uri;
}

Plain meaning:

If someone visits the HTTP version, send them to the HTTPS version.

Only enable this after HTTPS is actually working. If the certificate is not ready, you can create a redirect loop or a broken site.

Step 7: handle WebSockets if the app needs them

Some apps use WebSockets for realtime features. Examples include dashboards, chat, live reload, and some admin panels.

A WebSocket-friendly proxy usually includes:

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

If an app works normally but realtime features fail, check whether WebSocket support is missing in the proxy config.

Step 8: set upload size if needed

nginx has request size limits. If your app accepts file uploads, you may need:

client_max_body_size 20m;

Plain meaning:

Allow request bodies up to 20 MB.

Set this intentionally. Do not make it huge unless the app really needs it.

Step 9: save the config in the right place

On many Ubuntu/Debian servers, a site config goes here:

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

Then it is enabled with a symlink:

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

Some systems use:

/etc/nginx/conf.d/app.example.com.conf

Use the layout your server already uses. Do not mix styles unless you know why.

Step 10: test before reload

Always test nginx config before reloading:

sudo nginx -t

If the test passes, reload:

sudo systemctl reload nginx

If the test fails, nginx will show the file and line number. Fix that first. Do not restart blindly.

Step 11: test the public URL

After reload, test the domain:

curl -I https://app.example.com

Useful things to check:

  • does it return the expected status code?
  • does it redirect too many times?
  • does the app generate correct URLs?
  • do uploads work?
  • do WebSockets work if needed?
  • does the app log show the correct client IP?

Common mistakes

These are the mistakes I check first:

  • DNS does not point to the server;
  • app is not running;
  • nginx cannot reach the upstream address;
  • app listens on 127.0.0.1 inside a container, not the host;
  • wrong server_name;
  • forgot sudo nginx -t before reload;
  • HTTPS redirect enabled before certificate is ready;
  • missing WebSocket headers;
  • upload size too small.

My reverse proxy checklist

When I build a reverse proxy config, I check:

  1. What public domain should nginx answer for?
  2. Does DNS point to this server?
  3. What upstream URL can nginx reach?
  4. Do I need HTTPS in nginx?
  5. Should HTTP redirect to HTTPS?
  6. Does the app need WebSocket support?
  7. Does the app need larger uploads?
  8. Are forwarding headers included?
  9. Does sudo nginx -t pass?
  10. Did I reload nginx, not just edit the file?

The wizard does the boring syntax work. The important human part is knowing the domain, the upstream app address, and which extra behaviours the app needs.

Comments

Comments are welcome — please read the comment policy first. Powered by giscus and GitHub Discussions.