Candid photograph of a Linux server terminal with a soft olive-green glow against a dark slate background, conveying a calm technical atmosphere.

Step-by-step guides for system administrators — covering command-line basics, web server setup, and preparation material for technical interviews.

Browse Tutorials

Nginx as a reverse proxy for Node.js

Configuring Nginx as a reverse proxy for Node.js applications places a TLS-terminating gateway before the runtime. Nginx forwards HTTP and WebSocket requests to a private application port, while systemd supervises Node.js. This keeps certificates, request limits, and upstream routing under one administrator.

This guide targets RHEL, CentOS Stream, and Fedora. It assumes the application starts on port 3000 and uses app.example.com.au, an Australian public domain, so DNS and certificate testing remain realistic.

Prepare the server and firewall

Install Nginx and Certbot, then enable the service. On Fedora these packages are directly available; on RHEL or CentOS Stream, use a compatible repository such as EPEL when the base repositories lack Certbot.

sudo dnf install -y nginx certbot python3-certbot-nginx
sudo systemctl enable --now nginx

With firewalld, allow both web services; its definitions cover IPv4 and IPv6 where IPv6 is enabled. Keep port 3000 closed to the internet: Nginx should be the only public entry point, preventing traffic from bypassing TLS.

For teams without a full-time Linux operator, compare managed and self-managed deployments through project hosting options. Look for Australian-dollar invoicing and business-hours support that understands local holidays; a basic shared plan may not permit a long-running Node service.

Resolve the Australian domain

Create an A record for app.example.com.au pointing to the server’s static IPv4 address, and an AAAA record only when IPv6 is genuinely configured. For many Australian workloads, hosting in Sydney or Melbourne gives east-coast users a short path; for Perth and Adelaide, an edge cache can reduce repeated origin trips.

Set the DNS TTL to 300 seconds before any cutover, wait for propagation, then lower it again. This reduces stale answers during changes, although Australian broadband and mobile networks can still cache records beyond the advertised TTL.

Configure the upstream and proxy

Make the application bind to 127.0.0.1 rather than 0.0.0.0. In Express, enable loopback proxy trust only after the direct port has been closed:

app.set('trust proxy', 'loopback');
app.listen(3000, '127.0.0.1');

WebSocket upgrades need a shared connection map. Define it in /etc/nginx/conf.d/upgrade.conf:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

Create the initial HTTP virtual host in /etc/nginx/conf.d/app.conf:

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

    client_max_body_size 10m;

    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 X-Forwarded-Host $host;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_read_timeout 300s;
    }
}

Validate the files, reload Nginx, and let Certbot install the certificate and redirect:

sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d app.example.com.au --redirect

For server-sent events, disable proxy buffering; for large uploads, adjust client_max_body_size deliberately.

Run Node.js with systemd

Create a dedicated account, copy the repository into /srv/nodeapp with the correct ownership, and install locked production dependencies. Keep secrets in /etc/nodeapp.env with restrictive permissions rather than committing them to Git.

sudo useradd --system --home-dir /srv/nodeapp --create-home --shell /sbin/nologin nodeapp
cd /srv/nodeapp
sudo -u nodeapp npm ci --omit=dev

Use a minimal systemd unit with automatic restarts and hardening:

[Unit]
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=nodeapp
Group=nodeapp
WorkingDirectory=/srv/nodeapp
Environment=NODE_ENV=production
EnvironmentFile=/etc/nodeapp.env
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5
KillSignal=SIGTERM
TimeoutStopSec=30
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true

[Install]
WantedBy=multi-user.target

After editing the unit, load it, start the service and inspect its journal:

sudo systemctl daemon-reload
sudo systemctl enable --now nodeapp
sudo journalctl -u nodeapp -n 50 --no-pager

If the path to node differs, change ExecStart accordingly. For an Australian on-call organisation, flag a planned reload in the arvo handover so the local shift knows a reload is scheduled.

Verify traffic and diagnose failures

Test the backend directly, then through Nginx and the public hostname:

curl -I http://127.0.0.1:3000
curl -I -H 'Host: app.example.com.au' http://127.0.0.1
sudo nginx -t
sudo systemctl reload nginx
curl -I https://app.example.com.au

A 502 usually means Node.js is stopped, bound to the wrong address, or blocked by SELinux; check the process, listening sockets and error log. A 504 points to a slow event loop or an inadequate proxy_read_timeout, while 413 means the request body exceeds Nginx’s limit. The site’s Linux administration guides cover related systemd and firewall checks.

Where the Australian Privacy Act applies, source IP addresses in logs can be personal information, so define access and retention. Store timestamps in UTC and label dashboards AEST or AEDT for teams in Sydney and Melbourne; otherwise incident correlation becomes needlessly confusing.

Choose a deployment pattern

Three practical patterns cover most Australian deployments, from a low-cost VPS to a resilient edge:

Deployment pattern Request path Strength Main trade-off
One VPS Nginx proxies to one local Node service Low cost and simple operations A host failure stops the entire service
Docker pair Nginx proxies to a Node container on the same VPS Repeatable builds and cleaner releases Container maintenance adds operational work
Distributed edge A managed load balancer fronts multiple application nodes Failure tolerance and easier scaling Higher cost, release coordination and monitoring

Start with one VPS if traffic is modest and recovery can tolerate a short outage. For heavier workloads, run two Node instances behind Nginx, or use a managed load balancer across Sydney and Melbourne. The extra cost buys failover, but also requires health monitoring, consistent releases and tested recovery procedures.

Make the next step concrete: run sudo nginx -t against the staged configuration before any reload.