homeresume
 
   

Nginx local setup

Published June 21, 2026Last updated June 21, 20265 min read

Nginx is a reverse proxy and web server. On a local machine it can sit on port 80 and route traffic to apps running on other ports - a frontend on :3000, an API on :4000, or a Docker Compose stack with published ports.

This post covers install, config layout, reverse proxying, path-based routes, static files, and service commands on a local dev machine. It does not cover HTTPS, production hardening, or multi-domain TLS.

On Windows, use WSL2 with Ubuntu. On macOS or Linux, the same apt or brew install steps apply.

Prerequisites

  • WSL2 with Ubuntu (Windows) or a Linux/macOS shell
  • At least one app listening on a local port (for example :3000)
  • sudo access to install packages and edit /etc/nginx

Mental model

  • server - a virtual host block (listen, server_name) that handles requests for a hostname.
  • location - a path prefix inside a server (/, /api/) matched against the request URI.
  • proxy_pass - forwards the request to another HTTP server (your Node app, Compose service, etc.).
  • root - serves files from a directory on disk (built SPA, static HTML).

On Debian and Ubuntu, site configs live in /etc/nginx/sites-available/ and are enabled with symlinks in /etc/nginx/sites-enabled/. The main file /etc/nginx/nginx.conf includes those enabled sites.

Install

sudo apt update
sudo apt install nginx

Start Nginx and verify the default page:

sudo systemctl start nginx
curl -I http://localhost

You should get an HTTP 200 or 301 response.

Config layout

PathPurpose
/etc/nginx/nginx.confMain config; includes enabled sites
/etc/nginx/sites-available/Site config files (one file per app or domain)
/etc/nginx/sites-enabled/Symlinks to enabled sites
/var/log/nginx/access.logRequest log
/var/log/nginx/error.logErrors and config issues

Create a new site file:

sudo nano /etc/nginx/sites-available/local-dev

Enable it and disable the default site if it conflicts on port 80:

sudo ln -s /etc/nginx/sites-available/local-dev /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx

Always run nginx -t before reloading. A syntax error blocks reload and leaves the old config in place.

Proxy one service

Forward all traffic to an app on port 3000:

server {
listen 80;
server_name localhost;
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 Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}

The Upgrade and Connection headers allow WebSocket connections through the proxy (useful for Vite, Next.js dev server, etc.).

Use an upstream block when several backend servers share the same target name; for a single local app, proxy_pass with a URL is enough.

Path-based routes

Route /api/ to a backend on port 4000 and everything else to a frontend on port 3000:

server {
listen 80;
server_name localhost;
location /api/ {
proxy_pass http://127.0.0.1:4000/;
proxy_set_header Host $host;
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
}
}

Trailing slash in proxy_pass - proxy_pass http://127.0.0.1:4000/; (note the slash after the port) strips the matched /api/ prefix before forwarding. A request to /api/health reaches the backend as /health. Without the trailing slash, the backend receives /api/health.

Longer prefixes should use more specific location blocks. Nginx picks the best match for the request URI.

Static files

Serve a built frontend from disk instead of proxying:

server {
listen 80;
server_name localhost;
root /var/www/my-app;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}

try_files falls back to index.html for client-side routes in SPAs.

Commands

CommandPurpose
sudo systemctl start nginxStart Nginx
sudo systemctl stop nginxStop Nginx
sudo systemctl restart nginxFull restart (drops active connections)
sudo systemctl reload nginxReload config without dropping connections
sudo systemctl status nginxCheck service state
sudo nginx -tValidate config syntax
sudo nginx -s reloadReload without systemd
curl -I http://localhostSmoke test from the shell

Safe edit workflow: edit config → sudo nginx -tsudo systemctl reload nginx.

Use reload after config changes when the syntax test passes. Use restart when Nginx fails to start or after package upgrades.

Local networking on Windows + WSL

Nginx runs inside WSL. 127.0.0.1 inside WSL refers to the Linux VM, not processes on the Windows host.

  • App runs inside WSL or Docker Compose - use http://127.0.0.1:<port> in proxy_pass when the port is published to the WSL host (Compose ports: mapping works).

  • App runs on Windows only - use the Windows host IP from WSL:

    grep nameserver /etc/resolv.conf | awk '{ print $2 }'

    Put that IP in proxy_pass instead of 127.0.0.1, or run the app inside WSL/Compose so localhost routing stays simple.

From a Windows browser, http://localhost reaches Nginx in WSL when it listens on port 80.

Troubleshooting

  • Port 80 already in use - check what holds the port: sudo ss -tlnp | grep :80. Stop the conflicting service or change Nginx listen to another port (for example 8080).
  • 502 Bad Gateway - the upstream app is not running or the proxy_pass URL is wrong. Confirm the app responds: curl http://127.0.0.1:3000.
  • Config test fails - read the path and line from sudo nginx -t output; fix the file and test again before reload.
  • Unexpected routing - check /var/log/nginx/error.log and /var/log/nginx/access.log.
  • Docker Compose overview - run frontend and API in Compose, then point Nginx at the published host ports

Demo

Runnable configs for this post live in the nginx-local-setup-demo folder. Get access via code demos.