Nginx local setup
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) sudoaccess 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 updatesudo apt install nginx
Start Nginx and verify the default page:
sudo systemctl start nginxcurl -I http://localhost
You should get an HTTP 200 or 301 response.
Config layout
| Path | Purpose |
|---|---|
/etc/nginx/nginx.conf | Main 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.log | Request log |
/var/log/nginx/error.log | Errors 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/defaultsudo 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
| Command | Purpose |
|---|---|
sudo systemctl start nginx | Start Nginx |
sudo systemctl stop nginx | Stop Nginx |
sudo systemctl restart nginx | Full restart (drops active connections) |
sudo systemctl reload nginx | Reload config without dropping connections |
sudo systemctl status nginx | Check service state |
sudo nginx -t | Validate config syntax |
sudo nginx -s reload | Reload without systemd |
curl -I http://localhost | Smoke test from the shell |
Safe edit workflow: edit config → sudo nginx -t → sudo 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>inproxy_passwhen the port is published to the WSL host (Composeports: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_passinstead of127.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 Nginxlistento another port (for example8080). - 502 Bad Gateway - the upstream app is not running or the
proxy_passURL is wrong. Confirm the app responds:curl http://127.0.0.1:3000. - Config test fails - read the path and line from
sudo nginx -toutput; fix the file and test again before reload. - Unexpected routing - check
/var/log/nginx/error.logand/var/log/nginx/access.log.
Related posts
- 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.