How to Configure Nginx as a High-Performance HTTP/2 & HTTP/3 Reverse Proxy on Netcup
Placing a tuned Nginx edge reverse proxy in front of your Docker containers, Node.js/Go APIs, or Python Gunicorn workers on a Netcup server lets you terminate TLS 1.3 and HTTP/3 (QUIC) in one place, pool upstream keepalive connections, enforce per-IP rate limiting against credential stuffing, and serve static assets directly from Linux kernel page cache (sendfile on). This guide provides a complete production Nginx configuration tailored for multi-core AMD EPYC Netcup nodes.
ngx_http_v3_module), and Certbot systemd timers. PREREQUISITES // ENVIRONMENT REQUIREMENTS
CHECKLIST- ✓ A Netcup VPS or Root Server running Ubuntu 24.04 LTS or Debian 12 with ports 80/tcp, 443/tcp, and 443/udp open
- ✓ A domain name with DNS
A(IPv4) andAAAA(IPv6) records pointing to your Netcup server
Key Takeaways
EXECUTIVE BRIEFING-
01 Tune
worker_processes auto;andworker_rlimit_nofile 65535;in/etc/nginx/nginx.confso Nginx scales across all dedicated or shared AMD EPYC cores without hitting the default 1,024 file-descriptor ceiling. -
02 Use an
upstreamblock withkeepalive 32;andproxy_set_header Connection "";(or a WebSocket upgrade map) to eliminate ephemeral TCP port exhaustion between Nginx and local127.0.0.1backend containers. -
03 Configure a shared-memory rate-limiting zone (
limit_req_zone $binary_remote_addr) to protect/loginand/api/endpoints against Layer-7 floods.
📑 Quick Jump Table of Contents (3 Sections) ▼
Step 1: Install Nginx, Certbot & Tune Global Worker Limits (/etc/nginx/nginx.conf)
Ubuntu 24.04 LTS ships with Nginx 1.24+, and you can also use the official nginx.org mainline repository for Nginx 1.26/1.27 with full HTTP/3 QUIC support (ngx_http_v3_module). After installing nginx and certbot, your first systems engineering task is raising the worker file-descriptor limit (worker_rlimit_nofile) and configuring epoll event multiplexing in /etc/nginx/nginx.conf to match your Netcup server’s vCPU or dedicated EPYC core count.
Why is worker_rlimit_nofile 65535; mandatory on a reverse proxy? Every inbound client request proxied to a local backend container consumes two file descriptors inside the Nginx worker process: one socket for the external client connection on eth0 and a second socket for the loopback connection to 127.0.0.1:3000 (or an open file handle in /var/cache/nginx). Under the default Linux ceiling of 1024 open files per process, a 4-core server starts logging socket() failed (24: Too many open files) at fewer than 2,000 concurrent connections.
On multi-core AMD EPYC Netcup instances, you should also ensure sendfile on;, tcp_nopush on;, and tcp_nodelay on; are active inside the global http block. sendfile instructs the Linux kernel to copy static assets directly from the filesystem page cache to the virtio-net NIC ring buffer via DMA without copying bytes into Nginx user space, while tcp_nopush packs HTTP response headers and the first payload chunk into a single full MTU frame.
To protect backend application workers against credential-stuffing bots and Layer-7 floods, we also define a 10 MB shared-memory rate-limiting zone (limit_req_zone $binary_remote_addr zone=api_limit:10m rate=15r/s;) inside /etc/nginx/conf.d/00-netcup-tuning.conf. Because $binary_remote_addr stores IPv4 addresses in 4 bytes and IPv6 addresses in 16 bytes, a 10 MB zone tracks roughly 160,000 unique visitor IPs in RAM with zero disk I/O.
| Netcup Server Model | Physical ECC RAM | CPU Allocation | worker_processes & worker_connections | Upstream keepalive Pool Size |
|---|---|---|---|---|
| VPS nano G11.5s | 2 GB ECC | 2 vCPU Cores | auto (2 workers) × 4,096 connections | keepalive 16; |
| VPS 500 G12.5 | 4 GB ECC | 4 vCPU Cores | auto (4 workers) × 4,096 connections | keepalive 32; |
| VPS 1000 G12.5 | 8 GB ECC | 6 vCPU Cores | auto (6 workers) × 8,192 connections | keepalive 64; |
| RS 1000 G12.5 | 8 GB DDR5 ECC | 4 Dedicated AMD EPYC Zen 5 Cores | auto (4 workers) × 16,384 connections | keepalive 64; |
sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginx
# Verify Nginx version and compiled modules (TLS 1.3, HTTP/2, HTTP/3, RealIP)
nginx -V 2>&1 | tr ' ' '\n' | grep -E "http_v2|http_v3|realip|ssl" Command Explanation: Installs Nginx and the Certbot Nginx plugin, then inspects compiled modules to confirm HTTP/2, HTTP/3, and RealIP support.
--with-http_ssl_module
--with-http_v2_module
--with-http_v3_module
--with-http_realip_module # Hide Nginx version number in HTTP response headers and error pages
server_tokens off;
# Connection upgrade map for seamless WebSocket (wss://) proxying
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# Per-IP Rate Limiting Zone: 10 MB shared memory (~160,000 IP states), 15 req/sec baseline
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=15r/s;
limit_req_status 429;
# Upload size ceiling & keepalive timeouts
client_max_body_size 64M;
keepalive_timeout 65;
types_hash_max_size 4096; Command Explanation: Places global reverse-proxy maps and a 10 MB shared-memory rate-limiting zone inside /etc/nginx/conf.d/00-netcup-tuning.conf so every virtual host can reference $connection_upgrade and zone=api_limit.
Step 2: Create a Production Upstream & TLS 1.3 Reverse Proxy Server Block
Many tutorials show a naive proxy_pass http://127.0.0.1:3000; inside location / without an upstream keepalive pool. Because Nginx defaults to HTTP/1.0 with Connection: close for upstream requests, omitting a keepalive pool forces the Linux kernel to perform a full TCP three-way handshake and four-way teardown on lo (127.0.0.1) for every single HTTP request. Under sustained traffic, this fills the kernel’s TIME_WAIT table, exhausts the ephemeral port range (net.ipv4.ip_local_port_range), and triggers intermittent 502 Bad Gateway (99: Cannot assign requested address) errors.
Defining an upstream app_backend block with keepalive 32; instructs each Nginx worker process to maintain a pool of up to 32 idle, pre-connected HTTP/1.1 sockets to your local Docker container or Node.js/Go backend. When combined with proxy_http_version 1.1; and our $connection_upgrade map, Nginx reuses persistent upstream TCP sockets for normal HTTP requests while seamlessly upgrading to bidirectional wss:// tunnels whenever a client sends an Upgrade: websocket header.
For TLS termination, the configuration below enforces Mozilla’s Modern/Intermediate hybrid cipher profile (TLSv1.2 and TLSv1.3), allocates a 10 MB shared SSL session cache across all worker processes (ssl_session_cache shared:SSL:10m;, storing ~40,000 resumed sessions), and sets ssl_session_tickets off; to preserve strict Forward Secrecy. Note that if you experiment with TLS 1.3 zero-round-trip resumption (ssl_early_data on;), you must pass proxy_set_header Early-Data $ssl_early_data; to your backend so non-idempotent POST requests are protected against 0-RTT replay attacks.
Finally, modern web frameworks (including Next.js Auth.js, Keycloak, Laravel, and OAuth2 OIDC gateways) frequently emit large Set-Cookie JWT payloads or Content-Security-Policy headers that exceed Nginx’s default 4 KB buffer (upstream sent too big header while reading response header from upstream). Setting proxy_buffer_size 32k; and proxy_buffers 16 16k; eliminates header overflow 502 errors.
upstream app_backend {
server 127.0.0.1:3000 max_fails=3 fail_timeout=10s;
keepalive 32;
}
server {
listen 80;
listen [::]:80;
server_name app.example.com;
# Allow ACME HTTP-01 challenge verification, redirect everything else to HTTPS
location /.well-known/acme-challenge/ {
root /var/www/html;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name app.example.com;
# TLS Certificates (Managed by Certbot in Step 3)
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
# Modern Mozilla TLS 1.2 / 1.3 Cipher Suite Configuration
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;
ssl_session_tickets off;
# Hardened Security Headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Primary Application & WebSocket Reverse Proxy
location / {
limit_req zone=api_limit burst=30 nodelay;
proxy_pass http://app_backend;
proxy_http_version 1.1;
# Support both keepalive pooling and WebSocket upgrades
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Forward original client metadata to backend
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 X-Forwarded-Host $host;
# Proxy Buffer & Timeout Tuning
proxy_buffering on;
proxy_buffers 16 16k;
proxy_buffer_size 32k;
proxy_connect_timeout 10s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
} Command Explanation: Configures dual-stack IPv4/IPv6 listeners (listen 443 ssl; listen [::]:443 ssl;), modern http2 on; directive syntax, TLS 1.3 ciphers, HSTS headers, rate-limiting burst absorption (burst=30 nodelay), and WebSocket-aware upstream connection pooling.
Deprecated listen 443 ssl http2; syntax triggers warnings in Nginx 1.25.1+. Always declare listen 443 ssl; and listen [::]:443 ssl; followed by a standalone http2 on; directive.
Step 3: Issue Let’s Encrypt TLS Certificates & Verify Zero-Downtime Reloads
Before activating the HTTPS server block above (which references /etc/letsencrypt/live/app.example.com/fullchain.pem), bootstrap your certificate using certbot --nginx or certbot certonly --webroot. Make sure both listen 80; and listen [::]:80; are present in your HTTP server block: when a domain has an AAAA IPv6 record pointing to Netcup, Let’s Encrypt’s ACME validation authority connects over IPv6 first and will fail with Timeout during connect if Nginx only listens on IPv4.
Whenever you modify /etc/nginx/sites-available/*.conf or rotate TLS certificates in production, always run sudo nginx -t && sudo systemctl reload nginx rather than systemctl restart nginx. A reload sends SIGHUP to the master process, which parses the new configuration first (aborting safely without downtime if a typo exists), spawns a fresh generation of worker processes, and lets old workers finish in-flight HTTP and WebSocket requests before exiting.
If your Nginx build includes ngx_http_v3_module (standard in Nginx 1.25+ mainline and Ubuntu 24.04 updates), you can also enable HTTP/3 (QUIC) over UDP port 443 by adding listen 443 quic reuseport; and listen [::]:443 quic reuseport; to your primary default virtual host along with add_header Alt-Svc 'h3=":443"; ma=86400' always;. Remember that the kernel socket option reuseport must be declared on only one server block per IP/port pair across your entire Nginx configuration, and UDP port 443 (443/udp) must be allowed in both UFW and the Netcup SCP Firewall.
Finally, verify that Ubuntu’s certbot.timer systemd unit is active and execute sudo certbot renew --dry-run. The timer runs twice daily with a randomized delay, renewing certificates automatically 30 days prior to expiration and triggering a graceful Nginx reload hook.
# 1. Issue certificate via Certbot
sudo certbot --nginx -d app.example.com --non-interactive --agree-tos -m admin@example.com --redirect
# 2. Enable symlink, test Nginx configuration syntax, and hot-reload workers
sudo ln -sf /etc/nginx/sites-available/app.example.com.conf /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
# 3. Verify Certbot's twice-daily systemd renewal timer and run a dry-run test
sudo systemctl status certbot.timer --no-pager
sudo certbot renew --dry-run Command Explanation: Provisions the Let’s Encrypt certificate, removes the default placeholder site, verifies configuration syntax via nginx -t, and confirms automated renewal via certbot.timer.
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Congratulations, all simulated renewals succeeded Common Troubleshooting Scenarios
DIAGNOSTIC RUNBOOK [SYMPTOM] Nginx returns 502 Bad Gateway immediately after enabling the site
Root Cause: Your upstream application container (127.0.0.1:3000) is either stopped, listening on a different port, or crashed during startup.
Resolution: Check sudo ss -tulpn | grep 3000 to confirm the backend process is bound to 127.0.0.1:3000, and inspect /var/log/nginx/error.log (connect() failed (111: Connection refused)).
[SYMPTOM] WebSocket connections (wss://) disconnect every 60 seconds
Root Cause: Nginx’s default proxy_read_timeout is 60 seconds; if your WebSocket server does not send periodic ping/pong frames within 60 seconds, Nginx closes the idle socket.
Resolution: Either configure application-level WebSocket heartbeats every 30 seconds or increase proxy_read_timeout 3600s; inside the WebSocket location block.
[SYMPTOM] Certbot ACME HTTP-01 challenge fails on IPv6 (Timeout during connect)
Root Cause: You created an AAAA DNS record for your domain, and Let’s Encrypt prefers IPv6 validation, but your Nginx block is missing listen [::]:80; or your Netcup IPv6 gateway (fe80::1) is misconfigured.
Resolution: Ensure every server block includes both listen 80; and listen [::]:80;, and test outbound/inbound IPv6 connectivity using ping6 -c 3 2606:4700:4700::1111.
Frequently Asked Questions
VERIFIED Q&AWhy should I use systemctl reload nginx instead of systemctl restart nginx?
systemctl reload nginx sends SIGHUP to the Nginx master process, which validates /etc/nginx/nginx.conf first, spawns new worker processes with the updated config, and gracefully drains existing active HTTP/WebSocket connections on old workers with zero dropped requests.
Can Nginx proxy to multiple Docker apps on the same Netcup server?
Yes! That is the primary advantage of a host-level Nginx reverse proxy. Bind App A to 127.0.0.1:3001, App B to 127.0.0.1:3002, and App C to 127.0.0.1:3003, then create three separate /etc/nginx/sites-available/*.conf files routing each domain to its respective loopback port.
How do I enable HTTP/3 (QUIC) on Nginx 1.25+ on Netcup?
Add listen 443 quic reuseport; and listen [::]:443 quic reuseport; to your primary server block (note: reuseport should be specified on only one server block per IP/port pair), add add_header Alt-Svc 'h3=":443"; ma=86400' always;, and allow 443/udp in UFW and the Netcup SCP Firewall.
Why does Nginx return upstream sent too big header while reading response header from upstream?
Applications that set large JWT session cookies or CSP headers (such as Next.js, Keycloak, or Laravel) can exceed Nginx’s default 4 KB header buffer. Increasing proxy_buffer_size 32k; and proxy_buffers 16 16k; (as included in Step 2) resolves this completely.
REFERENCES // VERIFIED TECHNICAL SOURCES
- [1] Nginx Official Admin Guide — Reverse Proxy & HTTP/3 (https://nginx.org/en/docs/http/ngx_http_proxy_module.html)
- [2] Mozilla SSL Configuration Generator (Server-Side TLS) (https://ssl-config.mozilla.org/)