DevOps

Configure an Nginx Reverse Proxy for Node and Docker

Tech Setup5 min read
TS

Tech Setup

Published October 11, 2026 · Editorial policy

Configure an Nginx Reverse Proxy for Node and Docker

You have a Node.js application running inside a Docker container on port 3000, and you need to expose it to the public internet securely on standard ports 80 and 443. Exposing the Node container directly invites crashes from unhandled traffic spikes, lacks native SSL termination, and makes scaling to multiple containers a logistical nightmare. By the end of this guide, you will have Nginx running as a reverse proxy on the host machine, routing external HTTP and HTTPS traffic directly into your isolated Dockerized Node.js service with zero downtime during reloads.

The 60-Second TL;DR Fix

If you just want the working configuration files right now without reading the theory, apply these two files to your setup.

First, your Nginx server block configuration at /etc/nginx/sites-available/myapp:

server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        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;
    }
}

Second, your docker-compose.yml file to spin up the Node app, binding it strictly to the loopback interface on the host so external users cannot bypass Nginx:

version: '3.8'

services:
  web:
    build: .
    container_name: node_app
    restart: always
    ports:
      - "127.0.0.1:3000:3000"
    environment:
      - NODE_ENV=production

Preparing the Dockerized Node Environment

Before routing traffic through Nginx, your container must be configured to accept local proxy connections correctly. When Nginx sits in front of Node, Node stops seeing the end-user's IP address and instead sees the local proxy's IP.

Writing the Dockerfile

Create a standard production-ready Dockerfile in your project root:

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app .

EXPOSE 3000
CMD ["node", "index.js"]

Build and start the container using Docker Compose:

docker compose up -d --build

Verify the container is running and listening on the local interface:

curl -I http://127.0.0.1:3000

Expected output:

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 15
ETag: W/"f-5d41402abc..."
Date: Tue, 04 Mar 2025 12:00:00 GMT
Connection: keep-alive

If curl hangs or returns a connection refused error, check your container logs with docker compose logs to ensure your Node application has actually started up.

Installing and Configuring Nginx

Install Nginx on your host machine (Ubuntu/Debian assumed for package paths).

sudo apt update
sudo apt install nginx -y

Writing the Reverse Proxy Block

Remove the default Nginx site configuration to prevent routing conflicts:

sudo rm /etc/nginx/sites-enabled/default

Create a new configuration file for your application:

sudo nano /etc/nginx/sites-available/node-app

Paste the following production configuration, replacing api.example.com with your actual domain name:

upstream node_backend {
    server 127.0.0.1:3000;
    keepalive 64;
}

server {
    listen 80;
    server_name api.example.com;

    # Logging
    access_log /var/log/nginx/node-app.access.log;
    error_log /var/log/nginx/node-app.error.log;

    # Maximum upload size for payloads (e.g., JSON bodies or file uploads)
    client_max_body_size 10M;

    location / {
        proxy_pass http://node_backend;
        
        # Essential Proxy Headers
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        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;

        # WebSocket Support (if your Node app uses Socket.io or ws)
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # Timeouts for long-polling or heavy queries
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}

Enable the site by creating a symbolic link to sites-enabled:

sudo ln -s /etc/nginx/sites-available/node-app /etc/nginx/sites-enabled/

Testing and Reloading Nginx

Always test your Nginx configuration syntax before reloading the service to prevent taking down your entire web server:

sudo nginx -t

Expected output:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: the configuration file /etc/nginx/nginx.conf test is successful

If the test passes, reload Nginx to apply the changes gracefully without dropping active connections:

sudo systemctl reload nginx

Securing with Let's Encrypt SSL

An HTTP-only proxy is insufficient for modern web APIs. Secure your endpoint using Certbot to obtain a free SSL certificate and automatically update your Nginx configuration.

Install Certbot and the Nginx plugin:

sudo apt install certbot python3-certbot-nginx -y

Run Certbot against your domain:

sudo certbot --nginx -d api.example.com

Certbot will prompt you for an email address, ask you to agree to the terms of service, and then automatically modify your /etc/nginx/sites-available/node-app file to inject SSL certificates, redirect HTTP to HTTPS, and configure secure cipher suites.

Verify that auto-renewal is active on your system:

sudo systemctl status certbot.timer

Expected output:

● certbot.timer - Run certbot twice daily
     Loaded: loaded/-vendor/preset; enabled; vendor preset: persistent)
     Active: active (waiting) since Tue 2025-03-04 10:00:00 UTC; 2 hours ago

Troubleshooting Common Failures

When things break, the issue usually falls into one of three categories. Check these points before tearing down your configuration.

1. The 502 Bad Gateway Error

This is the most common error when setting up Nginx with Docker. It means Nginx cannot reach your Node application.

  • Cause A: Your Node app inside the container is bound to 127.0.0.1:3000 instead of 0.0.0.0:3000. Inside Docker, 127.0.0.1 refers to the container itself, not the host machine. However, because you are using Docker port mapping (-p 127.0.0.1:3000:3000), Nginx on the host can reach 127.0.0.1:3000. If your Node app listens on an internal IP inside the container that doesn't map out, connection fails. Ensure your Node app listens on 0.0.0.0 or simply relies on the default behavior of listening on all interfaces (process.env.PORT || 3000).
  • Cause B: Docker bridge network changes or firewall blocks (UFW). Run sudo ufw allow 'Nginx Full' to ensure port 80 and 443 are open on the host firewall.

2. Infinite Redirect Loops

If your browser throws an error stating that the page has redirected too many times, your X-Forwarded-Proto header is likely misconfigured.

  • The Fix: Ensure your Nginx configuration includes proxy_set_header X-Forwarded-Forwarded-Proto $scheme;. If your Node application uses frameworks like Express and trusts proxy headers (app.set('trust proxy', 1)), it will detect that the incoming request was HTTPS, preventing it from trying to force an HTTP-to-HTTPS redirect loop of its own.

3. Static Assets or Large Payloads Dropping

If your Node application serves large JSON payloads or file uploads and they fail silently or return a 413 Payload Too Large error:

  • The Fix: You missed client_max_body_size inside your server block or http context in /etc/nginx/nginx.conf. Increase it to match your requirements (e.g., client_max_body_size 50M;).

Alternatives and Future-Proofing

While Nginx is the industry-standard Swiss Army knife for reverse proxying, modern containerized architectures often evaluate alternatives depending on scale:

  • Traefik: If you plan to run multiple dynamic Docker containers with auto-scaling, Traefik natively reads Docker labels from your docker-compose.yml file and auto-configures its own routing and SSL certificates without requiring manual Nginx file updates.
  • Caddy: If you want automatic HTTPS out of the box without dealing with Certbot cron jobs, Caddy's configuration file format is significantly shorter and simpler than Nginx.
  • Scaling Nginx: As your Node app handles more traffic, expand your upstream block in Nginx to include multiple container instances or load-balance across different backend servers using least_conn; or ip_hash; directives to distribute the compute load evenly.