nginx in reverse-proxy mode is the standard front door for self-hosted services: one machine, one pair of ports (80/443), many backends — each with its own hostname or path, all sharing TLS and logging. A first proxy is a twenty-line config away.

The idea #

The shape of every reverse proxy
client -> nginx :443 -> app on 127.0.0.1:8080
                     -> app on 127.0.0.1:3000

Clients speak to nginx; nginx forwards to services that never face the internet directly. That indirection buys TLS in one place, virtual hosting for several domains, and zero exposure for the apps.

A first proxy #

/etc/nginx/sites-available/aospix
server {
    listen 80;
    server_name app.aospix.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        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;
    }
}

Enable, test, reload — the loop that never changes:

The nginx workflow
$ sudo ln -s /etc/nginx/sites-available/aospix /etc/nginx/sites-enabled/
$ sudo nginx -t                  # config test — always
$ sudo systemctl reload nginx    # zero-downtime reload

The three X-Forwarded-* headers are not decoration: they are how the backend learns the real client IP and scheme, and apps behind proxies mis-render URLs without them.

The patterns that come next #

PatternConfiguration
Serve static, proxy the APIlocation /api/ { proxy_pass ...; } and a root for files
WebSocketsproxy_http_version 1.1; + Upgrade/Connection headers
Several apps by nameOne server {} block per server_name
Larger uploadsclient_max_body_size 20m;
Timeouts for slow appsproxy_read_timeout 90s;

TLS for the same block is one certbot command away — it edits the config, obtains certificates, and arranges renewal — and the concepts behind the pattern are worth one read.

When something does not work #

The debugging ladder, in order:

Four steps that find nearly everything
$ sudo nginx -T | grep -A5 server_name    # what config is actually loaded
$ curl -H 'Host: app.aospix.com' http://localhost/   # nginx layer
$ curl http://127.0.0.1:8080/health       # backend layer
$ sudo tail -f /var/log/nginx/error.log   # the answers

Step 2 is the trick worth remembering: curl with a Host header tests the virtual host without touching DNS — splitting "nginx is wrong" from "DNS points elsewhere" (dig settles the latter).

Reload, never restart, for config changes — reload hands over sockets gracefully. And keep nginx -t between every edit: it parses the whole config and catches the missing-semicolon outage before your users do.

For the certificate side, see certbot; for the automatic-TLS alternative, Caddy. The network series continues.