Sign inGet started
Infrastructure · Nginx

Nginx config generator (reverse proxy, static, load balancing, PHP-FPM)

Build a production-ready Nginx server{} block for four different use cases: reverse proxy with rate limiting/auth/security headers, static sites with SPA fallback and caching, load balancing across multiple backends, or WordPress-style PHP-FPM. With Certbot SSL in every mode.

Need a VPS to run this on?VPS with real NVMe and a dedicated IP — perfect for putting Nginx in front of your API, your Node/Next.js app, your static site or your WordPress install.
View plans
nginx.conf
# Redirige todo el tráfico HTTP a HTTPS.
server {
    listen 80;
    listen [::]:80;
    server_name miapp.com;

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name miapp.com;

    ssl_certificate /etc/letsencrypt/live/miapp.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/miapp.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;

    client_max_body_size 10m;

    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml;

    location / {
        proxy_pass http://127.0.0.1:3000;
        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;
    }
}

What is a reverse proxy and why use one?

A reverse proxy is a server (here, Nginx) that receives all external connections on a public port (80/443) and forwards them to your actual application, which runs on an internal port (e.g. 127.0.0.1:3000). This gets you centralized SSL, the ability to serve several different apps under the same port 443 by domain name, and compression or caching without touching your app's code.

The proxy_set_header and WebSocket gotcha

Without proxy_set_header X-Real-IP and X-Forwarded-For, your app sees EVERY connection as if it came from 127.0.0.1 (Nginx's own IP) — silently breaking any logic that depends on the real client IP (rate-limiting, geolocation, logs). And without the proxy_http_version 1.1 + Upgrade + Connection "upgrade" trio, a WebSocket connection drops right after connecting, with no obvious error on the client side — it just looks like it "sometimes works and sometimes doesn't".

SSL: this builds the file, not the certificate

The generated SSL block points to the paths where Certbot places certificates by default (/etc/letsencrypt/live/your-domain/). This generator doesn't get the certificate for you — run certbot certonly (or certbot --nginx) separately before reloading Nginx with this config.

Rate limiting, basic auth and security headers

limit_req_zone: why burst matters as much as the rate

limit_req_zone defines a shared token bucket (per IP, thanks to $binary_remote_addr) that refills at a fixed rate — e.g. rate=10r/s means "10 requests per second on average". The important detail is burst: without it (or at 0), Nginx rejects with a 503 any burst that exceeds that exact rate, even from a real user double-clicking or loading a page with several assets at once. burst=20 gives it room to absorb those legitimate bursts before it starts blocking, and nodelay makes requests within the burst get processed immediately instead of being queued with delay.

Basic auth, security headers and error pages: when to use them

auth_basic is ideal for protecting internal dashboards (Grafana, phpMyAdmin, an admin panel with no login of its own) with an extra layer without touching the app — but it travels as plain encoded text, so it only makes sense with SSL enabled. Security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy and, with SSL, Strict-Transport-Security) cost nothing in performance and close off common clickjacking and MIME-sniffing vectors. Custom error pages keep a user from seeing Nginx's default page (or worse, a stack trace) when something fails.

Static sites, load balancing and PHP-FPM: the other three modes

SPA vs plain static site: the difference is in try_files

A SPA (React, Vue, or a Next.js static export) handles its own client-side routing with JavaScript — if someone lands directly on /profile and Nginx looks for a profile.html file that doesn't exist, without a fallback it returns a real 404. try_files $uri $uri/ /index.html fixes this: if the file doesn't exist, it serves index.html anyway and lets the JS router decide what to show. A plain static site (generated HTML, no client router), on the other hand, should use a real =404, so broken URLs actually return 404 instead of showing the homepage.

Load balancing: round-robin, least_conn and ip_hash

With no explicit method, Nginx distributes requests round-robin (one to each backend in turn, weighted if you use weight). least_conn instead sends each new request to whichever backend currently has the fewest active connections — better when your requests take very different amounts of time. ip_hash is the one that gives you "session stickiness": the same client IP always lands on the same backend, useful if your app keeps sessions in local memory instead of a shared store (Redis, a database) — without ip_hash, a user could log in on one backend and land on a different one on the next request that doesn't know them.

PHP-FPM: sockets vary by PHP version

The PHP-FPM socket (/run/php/php8.3-fpm.sock in the example) includes the PHP version in the filename, and that version depends on what you installed — php8.1-fpm, php8.2-fpm, php8.3-fpm are separate packages that run side by side if installed. If your fastcgi_pass points at a socket that doesn't exist, you'll get a 502 Bad Gateway. To find out which one you have, run ls /run/php/ on the server and you'll see the real .sock file; you can also confirm the running service with systemctl status "php*-fpm".

FAQ

Where do I put this file?+

Save it as /etc/nginx/sites-available/your-domain.conf, create a symlink in /etc/nginx/sites-enabled/ pointing to that file, run nginx -t to validate the syntax, and if it comes back clean, reload with systemctl reload nginx (or service nginx reload).

Does this get me the SSL certificate?+

No — it only generates the config file that already assumes where Certbot/Let's Encrypt will place the certificate. Running certbot (certbot certonly --nginx -d your-domain, or certbot --nginx if you want it to configure everything itself) is still a separate step, before reloading Nginx with this config.

Does this work for Node.js or Next.js apps?+

Yes — it's exactly the standard pattern for putting Nginx in front of a Node/Next.js/Express app running on a local port (e.g. next start on port 3000): point the "backend upstream" at 127.0.0.1:3000 and you're done. If your app uses WebSocket (Next.js with HMR, Socket.IO, etc.), turn on the WebSocket toggle.

Will rate limiting block real users?+

Not if you leave a reasonable burst (20 is a good default for normal human traffic). The burst absorbs short, legitimate spikes — several images loading at once, a double click — without returning 503. It only starts blocking when sustained volume clearly exceeds the configured rate, which is exactly bot or attack behavior.

Which mode should I pick for a React or Vue SPA?+

The "Static site" mode with the SPA fallback toggle on. That generates try_files $uri $uri/ /index.html, which is what you need for the client-side router to handle routes like /profile or /dashboard without Nginx returning a 404 when you land directly on that URL.

What happens if a load balancer backend goes down?+

Nginx detects connection failures automatically (by default, after max_fails=1 failed attempt it marks the backend unavailable for fail_timeout=10s) and stops sending it traffic until it responds again — no extra config is needed for that basic behavior. For active health checks (pinging an endpoint even with no real traffic) you need the commercial Nginx Plus module; with open source Nginx, checking is always passive, based on real failures.

How do I know which PHP-FPM socket to use?+

Run ls /run/php/ on your server — you'll see something like php8.3-fpm.sock (the version number depends on which php-fpm package you installed). Use that exact name in the socket field; if it doesn't match, Nginx will return a 502 Bad Gateway.

Need a VPS to run this on?

VPS with real NVMe and a dedicated IP — perfect for putting Nginx in front of your API, your Node/Next.js app, your static site or your WordPress install.

View plans
SolumeCore — VPS/VDS Hosting