< Back to Blog

Nginx as a Reverse Proxy: The 20-Line Config That Runs My Production Site

TL;DR / Key Takeaways

  • A reverse proxy sits in front of your app process and handles SSL, routing, and incoming connections so your app doesn't have to.
  • The core Nginx config for a Next.js site is around 20 lines. Most of it is boilerplate you write once and never touch again.
  • Let's Encrypt via certbot gives you free, auto-renewing SSL in about 10 minutes on any Ubuntu VPS.
  • The same RackNerd VPS running this blog also runs the Predict & Profit trading bots. One $6/month machine. No managed hosting tax.

Why I'm Not Paying $50/Month for Hosting

Vercel is fine. Netlify is fine. Render is fine. They are also priced for people who do not want to think about servers.

I am a data engineer. I think about servers all day. I am not paying a $40/month markup to avoid a config file I can write in 15 minutes.

The predictandprofit.io website is a Next.js app running in standalone mode on a RackNerd VPS that costs $6/month. The same machine runs the weather bot, the econ bot, a PostgreSQL instance, and this blog. Nginx sits in front of all of it as a reverse proxy. Let's Encrypt handles SSL. Certbot auto-renews the cert every 90 days without me doing anything.

That is the whole stack. Let me show you the config.


What a Reverse Proxy Actually Does

Your Next.js app starts a Node process and listens on a port. By default that port is 3000. It does not speak HTTPS. It does not know your domain name. It cannot serve multiple apps on the same machine without port conflicts. It has no rate limiting, no request buffering, no static file caching.

Nginx fixes all of that. It sits on ports 80 and 443, accepts every incoming request, terminates the SSL connection, and forwards the cleaned-up HTTP request to your app process on localhost:3000. Your app never touches the internet directly.

This is a reverse proxy. Client talks to Nginx. Nginx talks to your app. Your app never needs to know the difference.

The benefits are real:

  • SSL lives in one place, not in your app code
  • Multiple apps can run on the same machine on different domains
  • Nginx handles slow clients without blocking your Node process
  • Static files can be served directly without hitting Node at all
  • You get access logs, error logs, and rate limiting at the edge

The Config

Here is the sanitized Nginx config that runs predictandprofit.io. This is the actual file, minus the cert paths which certbot fills in automatically.

# /etc/nginx/sites-available/predictandprofit.io

server {
    listen 80;
    server_name predictandprofit.io www.predictandprofit.io;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name predictandprofit.io www.predictandprofit.io;

    ssl_certificate /etc/letsencrypt/live/predictandprofit.io/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/predictandprofit.io/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    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_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_cache_bypass $http_upgrade;
    }
}

That is 24 lines including blanks and comments. Let me walk through what each block does.


Block 1: The HTTP Redirect

server {
    listen 80;
    server_name predictandprofit.io www.predictandprofit.io;
    return 301 https://$host$request_uri;
}

Anyone who hits port 80 (plain HTTP) gets a permanent redirect to HTTPS. The $host variable preserves the domain, $request_uri preserves the path. So http://predictandprofit.io/blog becomes https://predictandprofit.io/blog. Nothing lands on your app over plain HTTP.

The 301 is a permanent redirect. Search engines will update their index. Browsers will cache it. Use 302 during testing if you are not sure you want the redirect to stick, then switch to 301 once it is working.


Block 2: The SSL Server Block

listen 443 ssl;
server_name predictandprofit.io www.predictandprofit.io;

This block handles HTTPS traffic. server_name tells Nginx which requests belong to this block. If you have multiple domains on the same machine, each gets its own server block with a different server_name.

The SSL lines come from certbot. You do not write them by hand. Certbot injects them automatically when you run the certificate setup. The options-ssl-nginx.conf file certbot generates handles protocol versions and cipher suites. You get a reasonable security baseline without having to research TLS configuration yourself.


Block 3: The Location Block and proxy_pass

location / {
    proxy_pass http://127.0.0.1:3000;
    ...
}

location / matches every request path. proxy_pass sends it to your app on localhost port 3000. That is the core of the reverse proxy. Everything else in this block is headers.

proxy_http_version 1.1 and the Upgrade/Connection headers are required for WebSocket support. Next.js uses WebSockets for hot module replacement in development. In production you probably do not need them, but they do not hurt and they save you debugging time if you ever add real-time features.

The X-Real-IP and X-Forwarded-For headers pass the client's actual IP address to your app. Without these, your app sees every request coming from 127.0.0.1. If you do any IP-based logging, rate limiting, or geo-detection inside your app, these headers are not optional.

X-Forwarded-Proto tells your app whether the original request was HTTP or HTTPS. Next.js uses this to generate correct absolute URLs in server-side rendering. Without it, SSR pages can produce http:// links on an HTTPS site.


Setting It Up: The Actual Commands

Enable the config and test it before reloading:

# Create the symlink to enable the site
sudo ln -s /etc/nginx/sites-available/predictandprofit.io \
           /etc/nginx/sites-enabled/

# Test the config for syntax errors
sudo nginx -t

# Reload without dropping connections
sudo systemctl reload nginx

nginx -t is the command you run every single time before reloading. It catches syntax errors before they take down your site. Make it a habit.


Let's Encrypt SSL in 10 Minutes

# Install certbot and the Nginx plugin
sudo apt install certbot python3-certbot-nginx

# Get a certificate and let certbot modify your Nginx config
sudo certbot --nginx -d predictandprofit.io -d www.predictandprofit.io

Certbot will ask for an email address, accept the terms, and then go get the certificate from Let's Encrypt. It modifies your Nginx config automatically, adding the ssl_certificate lines and the redirect from port 80. Then it reloads Nginx.

The cert is valid for 90 days. Certbot installs a systemd timer that runs twice daily and renews it automatically when it gets close to expiration. You will never manually renew a cert.

To verify the auto-renewal timer is active:

sudo systemctl status certbot.timer

You want to see active (waiting). If you see it, you are done with SSL forever.


Running Next.js in Standalone Mode

The Next.js app itself runs as a systemd service. Build it with standalone output so it does not need node_modules at runtime:

// next.config.js
module.exports = {
  output: 'standalone',
}

Then the systemd unit:

# /etc/systemd/system/predictandprofit.service

[Unit]
Description=Predict and Profit Next.js App
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/home/deploy/predictandprofit
ExecStart=/usr/bin/node .next/standalone/server.js
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
Environment=PORT=3000

[Install]
WantedBy=multi-user.target

Enable and start it:

sudo systemctl enable predictandprofit
sudo systemctl start predictandprofit
sudo systemctl status predictandprofit

The app starts on port 3000 at boot. Nginx proxies to it. The site is live.


The Cost Comparison

RackNerd 1GB VPS: $6/month. Runs the website, two trading bots, PostgreSQL, and this blog.

Vercel Pro: $20/month for one Next.js app with usage limits. No bots. No database. No other services.

Managed hosting with comparable specs: $40-80/month depending on provider.

The managed hosting argument is that you are buying time, not compute. That is true if you have never configured a Linux server. If you have, the time investment is the 20 minutes to write this config file, and then you never touch it again. The $6/month machine just runs.

One thing managed platforms genuinely do better: zero-config deployments via git push. I solved that with a deploy script and a webhook endpoint. Maybe I write that post next.


What This Does Not Cover

This config is for a single Next.js app on one domain. A few things I deliberately left out because they deserve their own post:

Rate limiting with limit_req_zone. Nginx can block brute force attempts at the edge before they hit your app. Worth adding for any site with auth endpoints.

Caching static assets with expires headers. The Next.js build output includes hashed filenames, so you can serve them with long cache lifetimes safely.

Multiple apps on one machine. The pattern is the same: each app gets a server block with its own server_name and a different proxy_pass port.


The Honest Part

This config has been running without modification since I deployed predictandprofit.io. No crashes, no cert issues, no Nginx problems. The trading bots have had plenty of bugs. The web server has not.

Nginx is not exciting. It is just a config file you write once and forget. That is exactly what infrastructure should be.

If you are running a side project and paying managed hosting rates because servers feel scary, this is the whole thing. Twenty lines, certbot, systemd. The $6/month machine handles the rest.