Nginx on Docker: Complete Guide with Defaults, Formulas, and Real Calculations

Published

No vague advice. Every directive: what it does, the default if you omit it, and exactly how to calculate the right value — with numbers.

Most Nginx guides say things like "set worker_connections to something reasonable." That's not helpful when your server crashes at 2 a.m.

This guide is different. For every important setting you'll get four things:

  1. What it does — in plain language
  2. Default value — what Nginx uses if you write nothing
  3. What goes wrong — if you leave the default or set it wrong
  4. How to calculate — step-by-step with real numbers

We'll use a concrete example server throughout:

Example server (our reference case):
4 vCPU · 8 GB RAM · 200 concurrent users · max upload 50 MB · largest API response 400 KB · backend response time up to 45 seconds (report exports)

Adjust the numbers for your server. The method stays the same.

Project structure and docker-compose.yml

nginx-stack/
├── docker-compose.yml
├── Dockerfile              (optional)
├── nginx/
│   ├── nginx.conf
│   ├── conf.d/default.conf
│   ├── snippets/ (ssl-params.conf, gzip.conf, security-headers.conf)
│   └── ssl/
└── html/
services:
  nginx:
    image: nginx:1.27-alpine
    container_name: nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/snippets:/etc/nginx/snippets:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
      - ./html:/usr/share/nginx/html:ro
      - nginx_logs:/var/log/nginx
    networks:
      - web
    ulimits:
      nofile:
        soft: 65535
        hard: 65535
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

networks:
  web:
    name: web

volumes:
  nginx_logs:

Full nginx.conf (with comments showing our calculated values)

user nginx;
worker_processes 4;              # calculated: = CPU cores
worker_rlimit_nofile 65535;      # calculated: ≥ worker_connections × 2

error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 2048;     # calculated: see below
    multi_accept on;
    use epoll;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format main '$remote_addr [$time_local] "$request" $status '
                    'rt=$request_time urt="$upstream_response_time"';

    access_log /var/log/nginx/access.log main;

    sendfile        on;
    tcp_nopush      on;
    tcp_nodelay     on;

    keepalive_timeout   65;
    keepalive_requests  500;

    server_tokens off;

    client_max_body_size        55m;    # calculated: 50 MB upload + 10%
    client_body_buffer_size     128k;
    client_header_buffer_size   2k;
    large_client_header_buffers 4 16k;

    limit_req_zone $binary_remote_addr zone=general:10m rate=20r/s;
    limit_conn_zone $binary_remote_addr zone=conn_limit:10m;

    proxy_http_version  1.1;
    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_set_header Connection        "";

    proxy_connect_timeout  10s;
    proxy_send_timeout     60s;
    proxy_read_timeout     55s;       # calculated: 45s backend + 10s

    proxy_buffer_size      16k;
    proxy_buffers          32 16k;    # calculated: ≥ 400 KB response
    proxy_busy_buffers_size 48k;

    include /etc/nginx/snippets/gzip.conf;
    include /etc/nginx/conf.d/*.conf;
}

Part 1 — Worker and connection settings

worker_processes

What it does: Number of Nginx worker processes. Each worker is a separate OS process that handles connections. Nginx does not use threads for connections — one worker = one process.

Default if omitted: 1 (only one worker, even on a 16-core server)

If you leave default: On a 4-core server, 3 cores sit idle. Max throughput is roughly 25% of what your CPU can handle.

How to calculate:

worker_processes = number of CPU cores (or "auto")
Our server has 4 vCPU → worker_processes 4
Check cores: nproc inside container → returns 4

worker_connections

What it does: Maximum simultaneous connections per worker. Includes: client connections + all proxy connections to backends + idle keepalive connections. One browser tab can hold multiple connections.

Default if omitted: 512

If you leave default: Max total connections = 1 worker × 512 = 512. With 200 concurrent users × ~6 connections each = 1,200 needed. You get connection refused errors and slow loading.

How to calculate — step by step:

  1. Count expected concurrent users: 200
  2. Connections per user (browser opens ~6 per page): 6
  3. Raw client connections: 200 × 6 = 1,200
  4. Add 20% headroom for spikes: 1,200 × 1.2 = 1,440
  5. Divide by worker_processes: 1,440 ÷ 4 = 360 per worker
  6. Round up to a clean number: 512 or 1024 or 2048
worker_connections = (concurrent_users × connections_per_user × 1.2) ÷ worker_processes
(200 × 6 × 1.2) ÷ 4 = 360 → we pick 2048 for future growth
Verify max capacity: 4 × 2048 = 8,192 total connections (enough for ~1,300 users at 6 conn each)

worker_rlimit_nofile

What it does: Sets the maximum number of open file descriptors (FDs) each worker process can use. Every TCP connection = 1 FD. Every open file (log, cache, static file) = 1 FD.

Default if omitted: OS default, often 1024 inside Docker containers

If you leave default: Even if worker_connections = 2048, the worker crashes at ~1024 open files with error: "too many open files (24)"

How to calculate:

worker_rlimit_nofile ≥ worker_connections × 2

The ×2 accounts for: one FD for client side + one FD for backend side of each proxied request.

worker_connections = 2048 → minimum = 2048 × 2 = 4096
We set 65535 (common production value, avoids recalculating later)
Also set Docker ulimits.nofile to 65535 in docker-compose (shown above)

multi_accept

What it does: When on, a worker accepts all new connections waiting in the queue at once. When off, accepts one at a time.

Default if omitted: off

If you leave default: Under high load, connection acceptance is slightly slower. Noticeable only above ~1,000 req/s.

Recommendation: on for production. No calculation needed.

use epoll

What it does: Tells Nginx which OS event mechanism to use for checking thousands of connections efficiently.

Default if omitted: Nginx auto-selects the best for your OS (epoll on Linux, kqueue on BSD/Mac)

If you leave default: Usually fine on Linux. Explicitly setting epoll is documentation, not a performance fix.

Recommendation: use epoll; on Linux Docker hosts. Remove on Windows/Mac Docker.

Part 2 — Client request limits

client_max_body_size

What it does: Maximum size of the request body (file uploads, large POST/PUT payloads). If a request exceeds this, Nginx returns 413 Request Entity Too Large immediately — the request never reaches your backend.

Default if omitted: 1m (1 megabyte)

If you leave default: Any upload over 1 MB fails with 413. This is the #1 cause of "upload works locally but not in production."

How to calculate — step by step:

  1. Find the largest file users can upload: 50 MB
  2. Add 10% margin: 50 × 1.1 = 55 MB
  3. Set: client_max_body_size 55m;
client_max_body_size = max_upload_size_MB × 1.1 → write as "Xm"
Upload limit 50 MB → client_max_body_size 55m;
JSON-only API, no uploads → client_max_body_size 2m; is enough
Video platform, 2 GB uploads → client_max_body_size 2200m;

client_body_buffer_size

What it does: Size of memory buffer for reading the request body. If the body fits in this buffer, it stays in RAM (fast). If larger, Nginx writes the body to a temp file on disk (slow — disk I/O per upload).

Default if omitted: 8k on older Nginx, 16k on Nginx 1.21+

If you leave default: A 50 MB upload always goes to disk temp file. Works, but uses disk and is slower. For a 2 KB JSON POST, 16k is fine.

How to calculate:

If most requests < 128 KB → set 128k (keeps typical requests in RAM)
If uploads are always large → keep 128k–512k (large files go to disk anyway; bigger buffer only helps medium-sized bodies)
Mixed API + 50 MB uploads → client_body_buffer_size 128k;
(50 MB uploads still spill to disk — that's normal and expected)

client_header_buffer_size

What it does: Buffer for reading the request header (URL, cookies, Authorization token, User-Agent, etc.). Most requests fit here.

Default if omitted: 1k (1024 bytes)

If you leave default: Fine for 99% of requests. Fails only with unusually large single headers.

Recommendation: 2k if you use large JWT tokens in cookies. Calculation: measure your largest Cookie header byte size + 20% margin.

large_client_header_buffers

What it does: Fallback buffers when the request header exceeds client_header_buffer_size. Format: count size — up to count buffers, each size bytes.

Default if omitted: 4 8k → total 4 × 8 KB = 32 KB max header size

If you leave default: Headers over 32 KB → 400 Bad Request error: "Request Header Or Cookie Too Large"

How to calculate:

  1. Measure total header size of your largest request (browser DevTools → Network → request headers size)
  2. Typical web app: 2–8 KB. Heavy auth cookies: 10–20 KB.
  3. Set: count × size ≥ measured_size × 1.5
Largest measured header = 12 KB → 12 × 1.5 = 18 KB needed
Default 4 × 8k = 32 KB → default is enough, no change needed
If headers reach 40 KB → set large_client_header_buffers 8 16k; (= 128 KB total)

Part 3 — Keepalive settings

keepalive_timeout

What it does: How many seconds Nginx keeps an idle client TCP connection open before closing it. While open, the same connection can serve the next HTTP request without a new TCP + TLS handshake.

Default if omitted: 75 seconds

If you leave default: Usually fine. 75s is a reasonable default.

How to choose:

We set 65s — slightly below default, frees slots a bit faster with 200 concurrent users.

keepalive_requests

What it does: Maximum number of HTTP requests on one keepalive connection before Nginx closes it (forces a fresh connection).

Default if omitted: 1000 (since Nginx 1.19.10; older versions: 100)

If you leave default: Fine for most cases.

How to calculate:

keepalive_requests = peak_requests_per_second_per_connection × keepalive_timeout

In practice: 100–1000. Lower values (100–500) help prevent memory leaks in buggy clients. We set 500.

Part 4 — Proxy buffers (reverse proxy)

When Nginx proxies a request to your backend, the backend's response flows through these buffers in Nginx's memory before being sent to the client.

proxy_buffer_size

What it does: Buffer for reading the response headers from the backend (status line, Content-Type, Set-Cookie, etc.).

Default if omitted: 4k or 8k (depends on platform)

If you leave default: If backend sends very large response headers (many Set-Cookie, long redirects), you get: "upstream sent too big header while reading response header"

How to calculate: Measure backend response header size. Usually 1–4 KB. Set 8k–16k to be safe.

We set 16k — safe default when unsure.

proxy_buffers

What it does: Buffers for reading the response body from the backend. Format: count size. Total memory = count × size per request.

Default if omitted: 8 4k or 8 8k → total 32 KB or 64 KB

If you leave default: Backend response body over 32–64 KB gets written to a temp file on disk (slower). Response over buffer + temp file limits can cause errors.

How to calculate — step by step:

  1. Measure largest response body from your backend (browser DevTools → response size, or curl -w '%{size_download}')
  2. Our largest API response: 400 KB
  3. Add 20% margin: 400 × 1.2 = 480 KB
  4. Pick count and size: 32 × 16k = 512 KB ≥ 480 KB ✓
proxy_buffers count size → count × size ≥ largest_response_bytes × 1.2
Response 400 KB → proxy_buffers 32 16k; (512 KB total)
Response 2 MB → proxy_buffers 64 32k; (2048 KB = 2 MB total)
Large file downloads → use proxy_buffering off; instead (stream directly, no buffer limit)

Memory impact: 200 concurrent proxied requests × 512 KB = 102 MB RAM for proxy buffers alone. Check your server has headroom.

proxy_busy_buffers_size

What it does: Maximum size of buffers that can be busy sending data to the client while still reading from the backend.

Default if omitted: Usually 8k or 16k, or auto-calculated as min(2×proxy_buffer_size, proxy_buffers_total − one_buffer)

If you set wrong: Nginx refuses to start: "proxy_busy_buffers_size must be less than the size of all proxy_buffers minus one buffer"

How to calculate:

proxy_busy_buffers_size = proxy_buffer_size × 2 (common rule)
Must be < (proxy_buffers count × size) − proxy_buffer_size
proxy_buffer_size = 16k, proxy_buffers = 32 × 16k = 512k
Max allowed = 512k − 16k = 496k
We set 48k (= 16k × 3, well under the limit) ✓

Part 5 — Proxy timeouts

proxy_connect_timeout

What it does: Max seconds to establish a TCP connection to the backend. Starts when Nginx tries to connect, ends when TCP handshake completes.

Default if omitted: 60s

If you leave default: A dead backend hangs for 60 seconds before Nginx returns 502. Too long for user-facing apps.

How to calculate: Backend on same Docker network connects in <1ms. Set 5–10s.

Same-host Docker network → proxy_connect_timeout 10s; (if no connection in 10s, backend is down)

proxy_send_timeout

What it does: Max seconds between two successive write operations when sending the request to the backend. If your upload to the backend stalls, this fires.

Default if omitted: 60s

How to calculate:

proxy_send_timeout ≥ (max_upload_size_MB ÷ upload_speed_MB_per_sec) + 10s
50 MB upload at 5 MB/s = 10 seconds → set 60s (default is fine)
500 MB upload at 1 MB/s = 500 seconds → set 600s

proxy_read_timeout

What it does: Max seconds of silence from the backend while Nginx waits for the response. Timer resets on each byte received. If backend stops sending for longer than this → 504 Gateway Timeout.

Default if omitted: 60s

If you leave default: Backend that takes 45 seconds to generate a report works. Backend that takes 90 seconds → 504 error.

How to calculate — step by step:

  1. Find slowest backend endpoint response time: 45 seconds (report export)
  2. Add 10 second margin: 45 + 10 = 55 seconds
  3. Set: proxy_read_timeout 55s;
proxy_read_timeout = slowest_backend_response_seconds + 10
Fast API only (all endpoints < 2s) → 30s is enough
Report generation 45s → 55s
WebSocket (long-lived) → 3600s (1 hour)

Part 6 — Rate limiting

limit_req_zone

What it does: Creates a shared memory zone that tracks request rates per IP (or other key). Must be defined in http {} block, then applied in server/location with limit_req.

Default if omitted: No rate limiting at all. Unlimited requests per IP.

Format: limit_req_zone $binary_remote_addr zone=NAME:SIZE rate=RATE;

SIZE (zone memory) — how to calculate:

1 MB zone stores ~16,000 unique IP addresses
10m zone stores ~160,000 unique IP addresses
Small site (<10K visitors/day) → zone=general:1m is enough
Medium site → zone=general:10m (our choice)
If zone memory fills, oldest entries are evicted (rate limit becomes inaccurate for those IPs)

RATE — how to calculate:

  1. Measure normal peak: e.g. 100 requests/second total across all users
  2. Estimate users at peak: 200
  3. Per-user fair share: 100 ÷ 200 = 0.5 req/s per user
  4. Set limit above fair share to avoid blocking normal users: 0.5 × 10 = 5–20 req/s per IP
We set rate=20r/s — allows 20 requests per second per IP. A normal user won't hit this. A bot doing 100 req/s gets throttled.

limit_req burst

What it does: Allows temporary spikes above the rate limit. Applied in location block: limit_req zone=general burst=N nodelay;

Default if omitted: burst=0 — strict rate, no spikes allowed. Excess requests get 503 immediately.

How to calculate:

burst = max_spike_requests_in_one_second − rate
rate = 20r/s. User opens page that loads 25 assets simultaneously.
burst = 25 − 20 = 5 (minimum). We set burst=20 for comfortable headroom.

nodelay means burst requests are processed immediately (not queued with delay).
Without nodelay: excess requests wait in queue (slower but smoother).

limit_conn

What it does: Limits how many simultaneous connections one IP can open to Nginx. Different from limit_req (which limits requests per second).

Default if omitted: No connection limit per IP.

How to calculate:

limit_conn = connections_per_user × 1.5
Normal browser: ~6 connections per page → set limit_conn 20 per IP
Prevents one IP from opening 500 connections and exhausting worker_connections

Part 7 — Gzip compression

gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_min_length 256;
gzip_types text/plain text/css application/json application/javascript;

gzip

What it does: Compresses response bodies before sending to the client. Reduces bandwidth, increases CPU usage.

Default if omitted: off — no compression, responses sent as-is

If you leave default: A 500 KB JSON response sends as 500 KB instead of ~50 KB compressed. Slower for users on mobile.

gzip_comp_level

What it does: Compression effort. Range 1 (fastest, weakest) to 9 (slowest, strongest).

Default if omitted: 1

How to choose (real numbers):

Set 5 — best balance. Only use 9 for static pre-compressed assets built at deploy time.

gzip_min_length

What it does: Don't compress responses smaller than this (compression header overhead isn't worth it for tiny files).

Default if omitted: 20 bytes

Recommendation: 256 or 1024 bytes. Compressing a 100-byte response can make it larger.

gzip_types

What it does: Which MIME types to compress. Only listed types are compressed.

Default if omitted: text/html only

If you leave default: JSON, CSS, JavaScript are NOT compressed. Your API responses stay uncompressed.

Recommendation: Always explicitly list text/plain, text/css, application/json, application/javascript, image/svg+xml.

Part 8 — Upstream and load balancing

upstream backend {
    server api:8080 max_fails=3 fail_timeout=30s;
    keepalive 32;
}

keepalive (inside upstream block)

What it does: Keeps idle TCP connections open to the backend for reuse. Without it, every request opens a new TCP connection (slow — 1–3ms overhead each).

Default if omitted: 0 (disabled — new connection per request)

How to calculate:

upstream keepalive = concurrent proxied requests to this backend
200 users, 50% of requests go to API, ~3 concurrent API requests per user at peak...
Rough estimate: 200 × 0.5 × 0.3 ≈ 30 concurrent → set keepalive 32

Requires in http {} block:
proxy_http_version 1.1;
proxy_set_header Connection "";

max_fails and fail_timeout

What they do: If a backend fails max_fails times within fail_timeout seconds, Nginx marks it unavailable for fail_timeout seconds.

Default if omitted: max_fails=1, fail_timeout=10s

Example: max_fails=3 fail_timeout=30s → 3 failures in 30s → backend removed from rotation for 30s.

weight (load balancing)

What it does: Distributes traffic proportionally across servers.

Default if omitted: weight=1 for all servers (equal distribution)

How to calculate:

server traffic % = server weight ÷ sum of all weights
server app1 weight=3, server app2 weight=1
Total weight = 4
app1 gets 3/4 = 75% of traffic
app2 gets 1/4 = 25% of traffic
Use when app1 has 3× the CPU/RAM of app2

Part 9 — SSL settings

ssl_session_cache

What it does: Caches SSL/TLS session data so returning visitors skip the full TLS handshake (saves 1 RTT ≈ 50–200ms).

Default if omitted: none in server block (no shared cache between workers)

How to calculate zone size:

1 MB shared cache ≈ 4,000 SSL sessions
10m cache ≈ 40,000 sessions
200 concurrent users, sessions last 1 day → need ~200 active sessions cached
1m is enough. We use 10m for growth.

ssl_session_timeout

What it does: How long a cached SSL session stays valid before requiring a full handshake again.

Default if omitted: 5m (5 minutes)

Recommendation: 1d (1 day) for better performance. Trade-off: slightly less secure if session keys are compromised (rare).

Site config — conf.d/default.conf

upstream backend {
    server api:8080 max_fails=3 fail_timeout=30s;
    keepalive 32;
}

server {
    listen 80;
    server_name example.com;
    location /health { return 200 "ok\n"; add_header Content-Type text/plain; }
    location / { return 301 https://$host$request_uri; }
}

server {
    listen 443 ssl;
    http2 on;
    server_name example.com;

    root /usr/share/nginx/html;
    ssl_certificate     /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/privkey.pem;
    include /etc/nginx/snippets/ssl-params.conf;

    limit_req zone=general burst=20 nodelay;
    limit_conn conn_limit 20;

    location = /health {
        return 200 "ok\n";
        add_header Content-Type text/plain;
    }

    location / {
        try_files $uri $uri/ =404;
    }

    location /api/ {
        proxy_pass http://backend;
        proxy_read_timeout 55s;
        proxy_buffer_size  16k;
        proxy_buffers      32 16k;
    }

    location ~ /\. { deny all; }
}

Worked example — full calculation summary for our server

Input: 4 vCPU · 8 GB RAM · 200 users · 6 conn/user · 50 MB uploads · 400 KB max response · 45s slowest backend

worker_processes = 4 (CPU cores)
worker_connections = (200 × 6 × 1.2) ÷ 4 = 360 → set 2048
Max capacity = 4 × 2048 = 8,192 connections
worker_rlimit_nofile = 2048 × 2 = 4096 → set 65535
client_max_body_size = 50 × 1.1 = 55m
proxy_buffers = 400 KB × 1.2 = 480 KB → 32 × 16k = 512 KB
proxy_read_timeout = 45 + 10 = 55s
proxy_busy_buffers_size = 16k × 2 = 32k → set 48k
upstream keepalive ≈ 30 concurrent API requests → set 32
limit_req rate = 20r/s per IP, burst=20
limit_conn = 6 conn × 1.5 ≈ 20
RAM for proxy buffers at peak = 30 concurrent × 512 KB = ~15 MB (fine on 8 GB server)

Master defaults table — quick lookup

Directive                  │ Default (if omitted)  │ Our value   │ Why we changed
───────────────────────────┼───────────────────────┼─────────────┼──────────────────────
worker_processes           │ 1                     │ 4           │ Match CPU cores
worker_connections         │ 512                   │ 2048        │ 200 users × 6 conn
worker_rlimit_nofile       │ OS (~1024 in Docker)  │ 65535       │ Avoid "too many open files"
client_max_body_size       │ 1m                    │ 55m         │ 50 MB uploads
client_body_buffer_size    │ 16k                   │ 128k        │ Keep medium bodies in RAM
client_header_buffer_size  │ 1k                    │ 2k          │ Large auth cookies
large_client_header_buffers│ 4 8k (=32k total)     │ 4 16k       │ More headroom
keepalive_timeout          │ 75s                   │ 65s         │ Free connections sooner
keepalive_requests         │ 1000                  │ 500         │ Slightly more aggressive recycle
proxy_connect_timeout      │ 60s                   │ 10s         │ Fail fast on dead backend
proxy_send_timeout         │ 60s                   │ 60s         │ Default OK for 50 MB uploads
proxy_read_timeout         │ 60s                   │ 55s         │ 45s backend + 10s margin
proxy_buffer_size          │ 4k–8k                 │ 16k         │ Safe for large headers
proxy_buffers              │ 8 4k–8k (=32–64k)     │ 32 16k      │ 400 KB responses
proxy_busy_buffers_size    │ 8k–16k                │ 48k         │ 16k × 3
gzip                       │ off                   │ on          │ Compress JSON/CSS/JS
gzip_comp_level            │ 1                     │ 5           │ Better compression
gzip_min_length            │ 20 bytes              │ 256 bytes   │ Skip tiny responses
gzip_types                 │ text/html only        │ explicit    │ Include JSON, CSS, JS
upstream keepalive         │ 0 (disabled)          │ 32          │ Reuse backend connections
server_tokens              │ on (shows version)    │ off         │ Security
limit_req                  │ none                  │ 20r/s       │ Anti-abuse

How to measure your own numbers

Don't guess — measure, then plug into the formulas above.

# CPU cores
nproc

# Open file limit inside container
ulimit -n

# Largest response body from your backend
curl -o /dev/null -s -w '%{size_download} bytes\n' https://example.com/api/biggest-endpoint

# Backend response time (p99 = slowest 1%)
# Use your monitoring tool, or:
curl -o /dev/null -s -w 'time_total: %{time_total}s\n' https://example.com/api/slow-endpoint

# Concurrent connections right now
docker compose exec nginx cat /proc/net/sockstat | grep TCP

# Nginx active connections
curl http://localhost/nginx_status   # requires stub_status module

Start, test, reload

docker compose up -d
docker compose exec nginx nginx -t          # always test first
docker compose exec nginx nginx -s reload   # apply without downtime

Troubleshooting — error → cause → fix

Error                              │ Likely cause                    │ Fix
───────────────────────────────────┼─────────────────────────────────┼────────────────────────────
413 Request Entity Too Large       │ client_max_body_size too small  │ Increase (default is only 1m)
504 Gateway Timeout                │ proxy_read_timeout too small    │ Increase (default 60s)
502 Bad Gateway                    │ Backend down or wrong address   │ Check proxy_pass + Docker network
upstream sent too big header       │ proxy_buffer_size too small     │ Set 16k or 32k (default 4–8k)
too many open files (24)           │ worker_rlimit_nofile too low    │ Set 65535 + Docker ulimits
400 Request Header Too Large       │ Header > 32k default           │ Increase large_client_header_buffers
503 Service Temporarily Unavailable│ Rate limit burst exceeded       │ Increase burst or rate
Changes not applied                │ Forgot reload                   │ nginx -t then nginx -s reload

Wrapping up

Every Nginx setting has a default, a purpose, and a formula. The defaults are conservative and often wrong for production — especially client_max_body_size (1m), worker_processes (1), gzip (off), and upstream keepalive (disabled).

Start with the worked example numbers, run your server, measure real traffic and response sizes, then recalculate. The method doesn't change — only the inputs do.

Have a specific error or directive you want calculated for your server specs? Drop your numbers in the comments.