Goals
-
Handle bursts safely on low RAM
-
Keep concurrency realistic (thousands, not hundreds of thousands)
-
Avoid swap thrashing / OOM
-
Get real client IP behind Cloudflare
-
Keep config minimal and stable
0) First Principles (don’t skip)
What limits you on 1–2GB is usually:
-
RAM per connection (keepalive + TLS + buffers)
-
CPU (TLS, HTTP/2, gzip, upstream latency)
-
FD limits (only if left at default 1024)
-
Backlog / SYN queue (bursts & attacks)
On low RAM, you must:
-
Keep timeouts short
-
Keep buffers moderate
-
Keep worker_connections sane
-
Ensure systemd LimitNOFILE is high
1) OS / systemd must-haves
1.1 Raise NGINX open files (FD limit)
Check:
ulimit -n
cat /proc/$(pidof nginx | awk '{print $1}')/limits | grep -i "open files"
If nginx shows 1024 soft, fix via systemd:
mkdir -p /etc/systemd/system/nginx.service.d
cat >/etc/systemd/system/nginx.service.d/override.conf <<'EOF'
[Service]
LimitNOFILE=100000
EOF
systemctl daemon-reload
systemctl restart nginx
1.2 Swap (safety net)
-
For 1GB RAM: 2GB swap is fine
-
For 1.5–2GB RAM: 1.5–2GB swap is fine
Swap is for stability, not capacity.
1.3 VM tuning (safe + proven)
cat >/etc/sysctl.d/99-nginx-memory.conf <<'EOF'
vm.swappiness=10
vm.vfs_cache_pressure=50
EOF
sysctl --system
1.4 Network tuning (safe for reverse proxy bursts)
cat >/etc/sysctl.d/99-nginx-network.conf <<'EOF'
# Faster cleanup of closed connections
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
# Larger accept queue for bursts
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
# Better SYN flood handling
net.ipv4.tcp_syncookies = 1
EOF
sysctl --system
Verify:
sysctl net.ipv4.tcp_fin_timeout net.core.somaxconn net.ipv4.tcp_max_syn_backlog
2) Global NGINX config (/etc/nginx/nginx.conf)
Use this as your minimal best-practice global config for 1–2GB reverse proxy:
user www-data;
worker_processes auto;
worker_cpu_affinity auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log warn;
include /etc/nginx/modules-enabled/*.conf;
worker_rlimit_nofile 100000;
events {
use epoll;
worker_connections 8192;
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
types_hash_max_size 2048;
server_tokens off;
server_name_in_redirect off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Cloudflare Real IP (global)
include /etc/nginx/snippets/cloudflare-realip.conf;
# Low-RAM safe timeouts
reset_timedout_connection on;
keepalive_timeout 10s;
keepalive_requests 1000;
client_header_timeout 10s;
client_body_timeout 10s;
send_timeout 10s;
client_max_body_size 50m;
# Moderate buffers (important for low RAM)
client_body_buffer_size 16k;
client_header_buffer_size 1k;
large_client_header_buffers 2 8k;
# FD cache
open_file_cache max=20000 inactive=30s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
# Logging format (helps debug upstream latency)
log_format main
'$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct=$upstream_connect_time '
'uht=$upstream_header_time urt=$upstream_response_time '
'ua="$upstream_addr" us="$upstream_status"';
access_log /var/log/nginx/access.log main;
# SSL defaults (certs live in server blocks)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1h;
ssl_session_tickets off;
ssl_ecdh_curve X25519:secp256r1;
resolver 1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 valid=300s ipv6=off;
resolver_timeout 5s;
# Gzip (safe defaults)
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types
text/plain text/css text/xml text/javascript
application/javascript application/json application/xml
application/rss+xml image/svg+xml;
# WebSocket helper (safe even if unused)
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# Reverse proxy defaults
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;
proxy_connect_timeout 5s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
proxy_buffering on;
proxy_buffer_size 16k;
proxy_buffers 8 16k;
proxy_busy_buffers_size 32k;
proxy_max_temp_file_size 0;
# Security headers
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;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Reload:
nginx -t && systemctl reload nginx
3) Cloudflare Real IP (required behind Cloudflare)
Create:
mkdir -p /etc/nginx/snippets
cat >/etc/nginx/snippets/cloudflare-realip.conf <<'EOF'
real_ip_header CF-Connecting-IP;
real_ip_recursive on;
# Cloudflare IPv4 ranges
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
# Cloudflare IPv6 ranges (keep even if server IPv6 disabled)
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2a06:98c0::/29;
set_real_ip_from 2c0f:f248::/32;
EOF
Test:
nginx -t && systemctl reload nginx
4) Vhost template (reverse proxy + backlog + websocket safe)
Backlog must be in
listenlines insideserver {}
Example vhost:
server {
listen 80 backlog=65535;
listen 443 ssl http2 backlog=65535;
server_name example.com;
ssl_certificate /etc/nginx/ssl/lego-ip/fullchain.crt;
ssl_certificate_key /etc/nginx/ssl/lego-ip/privkey.key;
# Optional: verify real IP quickly
location = /ip {
default_type text/plain;
return 200 "remote_addr=$remote_addr\ncf=$http_cf_connecting_ip\nxff=$http_x_forwarded_for\n";
}
location / {
proxy_pass http://127.0.0.1:8080;
# WebSocket safe headers (won’t break normal HTTP)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
Reload:
nginx -t && systemctl reload nginx
5) Recommended values (for 1–2GB)
NGINX
-
worker_processes auto -
worker_connections 8192(start here) -
If stable + enough RAM: try 16384 (but test)
-
worker_rlimit_nofile 100000(and systemd LimitNOFILE match)
Timeouts
-
keepalive_timeout 10s(important) -
proxy_read_timeout 30s(raise only if upstream needs it)
Buffers
-
Keep moderate (16k/8×16k) as in config
6) What NOT to do on 1–2GB
-
Don’t set
worker_connections 800000(RAM cannot handle it) -
Don’t set giant proxy buffers
-
Don’t keep keepalive very high (idle conns will eat RAM)
-
Don’t rely on swap for capacity (swap = latency + instability)
7) Monitoring commands (use during load)
Connection counts
ss -s
ss -Hnt state established '( sport = :443 or sport = :80 )' | wc -l
NGINX process limits
cat /proc/$(pidof nginx | awk '{print $1}')/limits | grep -i "open files"
Memory pressure + OOM checks
free -m
vmstat 1 5
dmesg -T | egrep -i 'oom|killed|out of memory' | tail -n 20
Backlog / drops
netstat -s | egrep -i 'listen|overflow|drop|retransmit|SYN'
8) Quick troubleshooting
“Real IP not showing”
-
Ensure
cloudflare-realip.confis included insidehttp {} -
Ensure you’re actually behind Cloudflare (orange cloud enabled)
-
Test endpoint
/ip
“Still capped at ~1000 connections”
-
systemd LimitNOFILE not applied
-
Check
/proc/.../limitsfor nginx (soft should be 100000)
“High latency under load”
-
Too many idle keepalives → keepalive_timeout too high
-
Upstream slow → check
$upstream_response_timein logs -
Swap usage → swappiness too high or RAM too low
9) Safe next steps (optional)
If attacks / abuse happen, add rate limits per IP in vhost:
-
limit_reqandlimit_conn
That’s all. Thank you for visiting us.
