DEV Community

Cover image for Nginx local setup
Željko Šević
Željko Šević

Posted on • Originally published at sevic.dev

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)
  • 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
Enter fullscreen mode Exit fullscreen mode

Start Nginx and verify the default page:

sudo systemctl start nginx
curl -I http://localhost
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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";
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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 -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 }'
Enter fullscreen mode Exit fullscreen mode

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.

Demo

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

Top comments (0)