# How I build an nginx reverse proxy config without memorising everything

A step-by-step beginner guide to reverse proxy basics, upstream apps, headers, TLS assumptions, and using the nginx Reverse Proxy Wizard safely.

- Date: 2026-09-27
- URL: https://ilham.dev/posts/how-to-build-nginx-reverse-proxy-config/
- Markdown: https://ilham.dev/posts/how-to-build-nginx-reverse-proxy-config/index.md
- Tags: nginx, devops, reverse-proxy, tools
- Reading time: 5 min


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

```nginx
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](/tools/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:

```text
browser -> nginx
```

nginx talks to the app:

```text
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:

```text
app.example.com
```

In nginx, this becomes:

```nginx
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:

```text
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:

```text
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](/tools/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:

```nginx
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:

```nginx
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.

```text
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.

```text
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:

```nginx
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:

```nginx
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:

```nginx
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:

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

Then it is enabled with a symlink:

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

Some systems use:

```text
/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:

```sh
sudo nginx -t
```

If the test passes, reload:

```sh
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:

```sh
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.
