Choosing the Right Linux Distribution and Preparing Your VPS
Alpine, Arch, OpenSUSE vs. Ubuntu/Debian – package manager nuances
Alpine Linux excels in minimalism; its apk packages are musl‑based and optimal for containers, but the nginx package lacks the official nginx‑full variant that ships with dynamic modules such as ngx_http_ssl_module. Arch Linux provides the most up‑to‑date nginx via pacman, yet the rolling‑release model introduces occasional ABI breaks that require immediate recompilation of custom modules. OpenSUSE Leap offers the zypper ecosystem with SELinux enabled by default, making it a solid choice for enterprises that need mandatory access control baked in. Ubuntu/Debian remain the safest bet for VPS providers because apt resolves dependencies automatically, the official nginx package includes the most common modules, and long‑term support releases guarantee security updates for at least five years.
When automating provisioning, align the distribution with the configuration management toolchain: apt integrates cleanly with Ansible's apt module, while yum/dnf pairs with the yum or dnf modules. For immutable infrastructure (e.g., images built with Packer), choose the distro whose base image you can lock to a specific SHA‑256 digest; this prevents drift in library versions that could affect proxy_pass TLS verification.
Installing Nginx from official repos and verifying the service
Begin by synchronising the package index and installing the nginx meta‑package. On Debian‑derived systems:
sudo apt update && sudo apt install -y nginx
On RHEL‑derived platforms use:
sudo dnf install -y nginx
After installation, enable and start the daemon with systemctl enable --now nginx. Verify the binary was compiled with the required modules:
nginx -V 2>&1 | grep -E 'http_ssl_module|http_v2_module|stream'
The output must contain --with-http_ssl_module and --with-http_v2_module for TLS termination and HTTP/2 support. Run sudo systemctl status nginx to ensure the master process is active and that the default test page is reachable on port 80. If the service fails to start, inspect /var/log/nginx/error.log for syntax errors or port conflicts.
Core Nginx Reverse Proxy Configuration for Any Backend
Crafting a robust server block with the proxy_pass directive
A production‑grade reverse proxy separates the public listener from the upstream definition. Declaring an upstream block allows you to pre‑populate keep‑alive pools, assign weights, and set failover thresholds:
upstream app_backend {
server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
server 127.0.0.1:3001 backup;
keepalive 32;
}
The accompanying server block enforces HTTPS, injects security headers, and forwards the request while preserving the original host header. Note the explicit proxy_set_header Connection "" directive; this clears the client‑side Connection header, enabling Nginx to reuse upstream keep‑alive sockets instead of closing them after each request.
server {
listen 443 ssl http2;
server_name www.example.com example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://app_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_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering on;
}
}
Finally, redirect plain HTTP to HTTPS with a minimal server block that returns 301. This avoids mixed‑content warnings and guarantees that every request benefits from TLS termination at the edge.
Handling WebSocket, HTTP/2, and HTTP/3 traffic through the proxy
WebSocket connections require an upgrade handshake; Nginx must forward the Upgrade and Connection headers unchanged. The following location snippet ensures the handshake survives the proxy layer:
location /ws/ {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
HTTP/2 is enabled automatically with the http2 flag on the listen directive. For HTTP/3 (QUIC), the listen 443 ssl http3 reuseport directive must be added and the quic module compiled. The server block then requires a separate ssl_certificate and a ssl_conf_command pointing to a quic specific profile. Because HTTP/3 runs over UDP, ensure your firewall permits inbound UDP 443 and adjust worker_processes to match the number of CPU cores for optimal packet processing.
When combining WebSocket and HTTP/2 on the same virtual host, keep the proxy_set_header Connection "" directive confined to the generic location; the WebSocket block overrides it with the required “Upgrade”. This dual‑stack configuration allows modern browsers to negotiate the highest protocol version while maintaining legacy compatibility for long‑running socket streams.
Securing the Proxy Layer – SSL Termination, OCSP Stapling, and Advanced Headers
Generating and deploying TLS certificates (Let’s Encrypt, self‑signed, internal PKI)
Let’s Encrypt remains the preferred source for publicly trusted certificates. Automate issuance with certbot and its Nginx plugin, which writes the certificate files directly into /etc/letsencrypt/live/ and reloads the service on renewal. For internal services that never leave the corporate perimeter, a self‑signed CA hierarchy reduces cost and eliminates rate‑limits. Create a root CA:
openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt -subj "/CN=Internal CA"
Then sign leaf certificates for each domain:
openssl genrsa -out example.com.key 2048
openssl req -new -key example.com.key -out example.com.csr -subj "/CN=example.com"
openssl x509 -req -in example.com.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out example.com.crt -days 825 -sha256
Deploy the chain (concatenate example.com.crt and ca.crt) to /etc/nginx/ssl/example.com.bundle.pem and reference it in the ssl_certificate directive. Remember to configure ssl_trusted_certificate to point to the root CA so Nginx can validate client certificates if mutual TLS is required.
Enabling OCSP stapling and DNS‑based Authentication (DANE)
OCSP stapling offloads certificate revocation checks from the client by caching the OCSP response at the edge. Add the following to the TLS block:
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
For environments that require DNS‑Based Authentication of Named Entities (DANE), publish a TLSA record that binds the certificate or its hash to the domain name. Nginx does not validate DANE natively, but external tools such as unbound can perform the validation and expose the result via a local stub resolver. Configure resolver to point to the validating resolver and set ssl_verify_client optional_no_ca if you also demand client‑side certificate verification.
After editing, test stapling with openssl s_client -connect example.com:443 -servername example.com -status. The output must contain OCSP Response Data and a status of successful. For DANE, use drill TLSA example.com or dig +dnssec TLSA example.com to confirm the record propagates correctly.
Hardening response headers – CSP nonces, HSTS, Referrer‑Policy, and rate limiting
Content Security Policy (CSP) mitigates XSS by restricting the origins for scripts, styles, and media. Generate a per‑request nonce in Nginx using the ngx_http_secure_link_module or embed a random token via Lua (if ngx_http_lua_module is compiled). Example header:
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-$request_id'";
Enforce HTTPS Strict Transport Security (HSTS) with a max‑age of one year and include subdomains:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
Set Referrer-Policy to strict-origin-when-cross-origin to protect navigation data, and enable Permissions-Policy to restrict APIs such as geolocation or camera. Rate limiting prevents brute‑force attacks on authentication endpoints:
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/s;
server {
location /login {
limit_req zone=login burst=10 nodelay;
proxy_pass http://app_backend;
}
}
Combine these headers with the earlier security header block to create a defense‑in‑depth response profile that satisfies PCI‑DSS and CIS Benchmarks.
SELinux/AppArmor Adjustments and Firewall Rules for Smooth Proxy Operation
Updating SELinux policies or AppArmor profiles for proxy_pass connections
On SELinux‑enforced distributions (CentOS, Fedora, RHEL), the default httpd_can_network_connect boolean must be enabled to allow Nginx to reach backend sockets:
sudo setsebool -P httpd_can_network_connect on
If the backend uses non‑standard ports, extend the policy with a custom module:
cat > nginx_backend.te <<EOF
module nginx_backend 1.0;
require {
type httpd_t;
class tcp_socket name_connect;
}
# Allow Nginx to connect to port 3000
allow httpd_t self:tcp_socket name_connect;
EOF
sudo checkmodule -M -m -o nginx_backend.mod nginx_backend.te
sudo semodule_package -o nginx_backend.pp -m nginx_backend.mod
sudo semodule -i nginx_backend.pp
For AppArmor (Ubuntu, OpenSUSE), edit /etc/apparmor.d/usr.sbin.nginx and add:
/var/www/** r,
network inet stream,
network inet dgram,
Then reload:
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx
These adjustments ensure the mandatory access control layer does not silently block outbound connections, which would otherwise manifest as 502 errors in the client.
Configuring nftables/ufw to allow inbound 80/443 and outbound backend ports
Most VPS providers ship with a host‑level firewall. Using ufw (Ubuntu) the minimal rule set is:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow out to any port 3000,3001 proto tcp
sudo ufw enable
For a more granular nftables configuration, create a table filter with separate chains for inbound and outbound traffic. The following snippet opens the public ports and restricts backend traffic to loopback interfaces:
#!/usr/sbin/nft -f
table ip filter {
chain input {
type filter hook input priority 0; policy drop;
iif "lo" accept
ct state established,related accept
tcp dport {80, 443} accept
}
chain output {
type filter hook output priority 0; policy accept;
oif "lo" ip daddr 127.0.0.0/8 tcp dport {3000, 3001} accept
}
}
Load the script with sudo nft -f /etc/nftables.conf. Verify the rules with sudo nft list table ip filter. This approach isolates the proxy from the external network while allowing it to reach backend services bound to 127.0.0.1 or a private VLAN.
Automating Deployment with Infrastructure‑as‑Code
Ansible playbook to provision Nginx, upload configs, and reload safely
The following playbook demonstrates idempotent provisioning. It installs Nginx, copies a templated site configuration, ensures the firewall is open, and reloads the daemon only when the configuration changes.
- hosts: webservers
become: true
vars:
domain: example.com
upstream_servers:
- 127.0.0.1:3000
- 127.0.0.1:3001
tasks:
- name: Install Nginx
apt:
name: nginx
state: present
when: ansible_os_family == "Debian"
- name: Install SELinux boolean (RHEL family)
seboolean:
name: httpd_can_network_connect
state: true
persistent: true
when: ansible_os_family == "RedHat"
- name: Upload reverse‑proxy config
template:
src: nginx_reverse_proxy.conf.j2
dest: /etc/nginx/sites-available/{{ domain }}.conf
mode: "0644"
notify: Reload Nginx
- name: Enable site
file:
src: /etc/nginx/sites-available/{{ domain }}.conf
dest: /etc/nginx/sites-enabled/{{ domain }}.conf
state: link
notify: Reload Nginx
- name: Open firewall (UFW)
ufw:
rule: allow
name: "Nginx Full"
when: ansible_os_family == "Debian"
handlers:
- name: Reload Nginx
service:
name: nginx
state: reloaded
The nginx_reverse_proxy.conf.j2 template should contain the upstream block populated from upstream_servers. Because Ansible tracks file checksum changes, the handler runs only when the template differs from the existing file, guaranteeing zero‑downtime reloads.
Docker‑based reverse proxy using official Nginx image and bind mounts
Containerising the proxy provides isolation and version pinning. Pull the official image, mount a configuration directory, and expose ports 80 and 443 to the host:
docker run -d \
--name nginx-proxy \
-p 80:80 -p 443:443 \
-v /srv/nginx/conf.d:/etc/nginx/conf.d:ro \
-v /srv/nginx/ssl:/etc/letsencrypt/live:ro \
--restart unless-stopped \
nginx:1.27-alpine
Store the conf.d files in /srv/nginx/conf.d and keep the TLS material under /srv/nginx/ssl. Since the container runs as the nginx user (UID 101 in the Alpine image), ensure the mount points are readable by that UID. Use a Docker Compose file for multi‑service setups; define a depends_on relationship so the application containers start before the proxy, and employ a healthcheck that curls https://localhost/healthz inside the container to verify the chain is operational.
Zero‑Downtime Migration Strategies: Blue‑Green and Canary Deployments
Setting up upstream groups for staged traffic shifting
Define two distinct upstream blocks—blue and green—each pointing to a separate version of the application. The primary server block references a third upstream named active that uses the least_conn algorithm and contains only one of the two groups at a time. Switching traffic is a matter of editing the active definition and reloading Nginx:
upstream blue {
server 10.0.1.10:8080;
}
upstream green {
server 10.0.1.11:8080;
}
upstream active {
# Initially point to blue
include /etc/nginx/upstreams/active_blue.conf;
}
To promote green, replace active_blue.conf with active_green.conf (which contains server 10.0.1.11:8080;) and run nginx -s reload. Because Nginx reload is graceful, existing connections finish on the blue pods while new connections are routed to green, achieving a seamless cutover.
Using health checks and gracefully draining connections
Open‑source Nginx lacks native active health checks, but passive checks can be simulated with the max_fails and fail_timeout parameters. For true active probing, integrate the third‑party ngx_http_upstream_check_module or use Nginx Plus. Regardless of the method, configure proxy_next_upstream to skip unhealthy backends:
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 3;
During a migration, enable drain mode by temporarily reducing the keepalive count to zero for the old upstream and setting max_fails=1. Nginx will stop assigning new connections while existing ones are allowed to complete. Monitor nginx_upstream_requests_total and nginx_upstream_response_time_seconds metrics to confirm that the old pool is fully drained before decommissioning the legacy servers.
Monitoring, Alerting, and Performance Tuning for High‑Traffic Proxies
Exporting Nginx metrics to Prometheus and creating Alertmanager rules for 5xx spikes
Deploy the nginx-prometheus-exporter as a sidecar or separate service. It scrapes the stub_status endpoint and exposes counters for connections, requests, and response codes. Add to nginx.conf:
location /metrics {
stub_status;
allow 127.0.0.1;
deny all;
}
Run the exporter:
docker run -d -p 9113:9113 \
-v /var/run/nginx.sock:/var/run/nginx.sock \
nginx/nginx-prometheus-exporter
In Prometheus, scrape http://localhost:9113/metrics. Create an alert that fires when the rate of 5xx responses exceeds 1 % of total traffic over a five‑minute window:
ALERT NginxHighErrorRate
IF sum(rate(nginx_http_requests_total{status=~"5.."}[5m])) /
sum(rate(nginx_http_requests_total[5m])) > 0.01
FOR 5m
LABELS {severity="critical"}
ANNOTATIONS {
summary="High HTTP 5xx rate on {{ $labels.instance }}",
description="Error rate is {{ $value | printf \"%.2f\" }}% over the last 5 minutes."
}
Link Alertmanager to Slack or PagerDuty so on‑call engineers receive immediate notifications.
Fine‑tuning worker_processes, worker_connections, and open file limits
Nginx’s concurrency model is driven by the product of worker_processes and worker_connections. On a VPS with 2 vCPUs and 1 GB RAM, a safe starting point is:
worker_processes auto; # resolves to 2
worker_connections 4096;
events {
use epoll;
multi_accept on;
}
This yields a theoretical maximum of 8 192 simultaneous connections, well below the kernel’s default ulimit -n of 1024. Increase the limit in /etc/security/limits.conf or a systemd drop‑in:
[Service]
LimitNOFILE=65536
After reloading systemd, verify with cat /proc/$(pidof nginx)/limits | grep "Max open files". Adjust client_body_buffer_size and proxy_buffer_size based on average payload size; larger buffers reduce disk I/O for big uploads but consume more RAM per worker.
Leveraging ngx_stream for TCP/UDP proxying and load balancing
When the backend includes non‑HTTP services (e.g., MySQL or Redis), the ngx_stream module provides L4 proxying. Define a stream block:
stream {
upstream mysql_cluster {
server 10.0.2.10:3306 max_fails=2 fail_timeout=30s;
server 10.0.2.11:3306 backup;
}
server {
listen 3306;
proxy_pass mysql_cluster;
proxy_timeout 10s;
proxy_connect_timeout 5s;
}
}
For UDP‑based services such as DNS, replace proxy_pass with proxy_pass udp://backend; and set proxy_responses 1. Monitoring of stream traffic can be achieved with nginx-stream-exporter, which emits connection counts and bandwidth usage to Prometheus. Combining HTTP and stream blocks in a single Nginx instance reduces the operational footprint on a small VPS while still providing load balancing and health‑check capabilities across protocol layers.
Take Action Now
🚀 Download the Complete “Nginx Reverse Proxy on Ubuntu 24.04” Playbook
Get a ready‑to‑run Ansible playbook that installs Nginx, configures a production‑grade reverse proxy, applies SELinux/AppArmor tweaks, sets up UFW/nftables, and adds Prometheus metrics—all tuned for Ubuntu 24.04 LTS.
Download Playbook (YML)🔧 Grab the Docker Compose Template for a Secure Nginx Proxy
This docker‑compose.yml includes volume mounts for Let’s Encrypt certificates, a healthcheck, and an auto‑restart policy. Perfect for quick deployments on any Linux VPS.
Download Compose File📚 Free PDF: “Advanced Nginx Hardening – OCSP, DANE & CSP”
Step‑by‑step guide with command‑by‑command examples, ready to paste into your Nginx config. Ideal for security‑focused admins.
Get the PDF💼 Need Enterprise‑Grade Performance?
Upgrade to a self‑managed dedicated server for unmatched CPU, RAM, and network throughput. Perfect for high‑traffic sites, gaming, GPU workloads, and media streaming.
Explore Dedicated Servers