$ sudo apt update && sudo apt install -y nginx
$ sudo yum install -y epel-release nginx && sudo systemctl enable --now nginx
$ sudo systemctl status nginx
$ sudo systemctl reload nginx
$ sudo systemctl restart nginx
$ sudo nginx -t # test config
$ nginx -V # built modules
/etc/nginx/nginx.conf
(main config)/etc/nginx/conf.d/*.conf
(drop‑ins)/etc/nginx/sites-available/
+ sites-enabled/
(Debian style)/var/www/html
(default docroot)logs
: /var/log/nginx/access.log
, /var/log/nginx/error.log
# /etc/nginx/conf.d/example.conf
server {
listen 80;
server_name example.com;
root /var/www/example/public;
location / {
try_files $uri $uri/ =404;
}
}
main
(global)events
(worker connections)http
→ server
→ location
stream
(TCP/UDP)upstream
(load balancers)user www-data;
worker_processes auto;
events { worker_connections 1024; }
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
# servers / includes go here...
}
location
match order:
=
^~
(no regex if matched)~
/ ~*
(first match)try_files
evaluates in order then falls back.location = /healthz { return 204; }
location ^~ /static/ { expires 7d; }
location ~* \.(png|jpg|css|js)$ { expires 7d; }
location / { try_files $uri $uri/ /index.html; }
http {
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/snippets/*.conf; # Ubuntu/Debian
}
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example/public;
index index.html index.htm;
}
server {
listen 80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
# Force non-www
server {
listen 80;
server_name www.example.com;
return 301 $scheme://example.com$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
root /var/www/example/public;
}
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
$ sudo apt install -y certbot python3-certbot-nginx
$ sudo certbot --nginx -d example.com -d www.example.com
$ sudo systemctl list-timers | grep certbot # auto-renew
upstream app {
server 127.0.0.1:3000;
# server unix:/run/app.sock; # alternative
}
server {
listen 80;
server_name api.example.com;
location / {
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_pass http://app;
}
}
location /socket.io/ {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://app;
}
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffering on;
proxy_buffers 16 16k;
proxy_busy_buffers_size 24k;
location /assets/ {
alias /var/www/example/assets/;
access_log off;
expires 7d;
add_header Cache-Control "public, max-age=604800, immutable";
}
gzip on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
gzip_min_length 1024;
gzip_comp_level 5;
brotli on;
brotli_comp_level 5;
brotli_types text/plain text/css application/javascript application/json image/svg+xml;
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=micro:10m max_size=1g inactive=10m use_temp_path=off;
map $request_method $no_cache { default 0; POST 1; PUT 1; PATCH 1; DELETE 1; }
location /api/ {
proxy_cache micro;
proxy_cache_bypass $no_cache;
proxy_no_cache $no_cache;
proxy_cache_valid 200 301 302 10s;
proxy_cache_valid any 1s;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://app;
}
# Skip cache when logged in (example cookie)
map $http_cookie $logged_in {
default 0;
~*"(session|auth|logged_in)" 1;
}
proxy_cache_bypass $logged_in;
proxy_no_cache $logged_in;
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_buffers 16 16k;
fastcgi_read_timeout 60s;
}
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~* \.(?:ini|log|sh|sql|bak)$ { deny all; }
location ~ /\.(?!well-known) { deny all; }
# 10 req/s with burst 20 per IP
limit_req_zone $binary_remote_addr zone=reqs:10m rate=10r/s;
server {
location /api/ {
limit_req zone=reqs burst=20 nodelay;
}
}
limit_conn_zone $binary_remote_addr zone=conns:10m;
server {
location /download/ {
limit_conn conns 10;
}
}
client_max_body_size 25m;
client_body_timeout 30s;
keepalive_timeout 65s;
server_tokens off;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
location /admin/ {
allow 192.168.0.0/16;
deny all;
}
location /api/ {
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Credentials "true" always;
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
add_header Access-Control-Allow-Headers "Authorization, Content-Type";
return 204;
}
proxy_pass http://app;
}
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'$request_time $upstream_response_time';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
location /healthz { access_log off; }
$ sudo nginx -t
$ sudo nginx -s reload
$ tail -f /var/log/nginx/error.log
Directive | Meaning |
---|---|
(default) | round‑robin |
least_conn |
least connections |
ip_hash |
sticky by client IP |
hash key |
hash by custom key |
upstream api_backends {
least_conn;
server 10.0.0.11:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.12:8080 max_fails=3 fail_timeout=30s;
# server backup.example:8080 backup;
}
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 3;
Variable | Description |
---|---|
$host |
Host header / server name |
$server_name |
Chosen server_name |
$remote_addr |
Client IP |
$http_user_agent |
User‑Agent |
$request_method |
GET/POST/... |
Variable | Description |
---|---|
$document_root |
Current root |
$realpath_root |
Symlink‑resolved root |
$request_uri |
Path + query |
$uri |
Normalized URI |
$args |
Raw query string |
Variable | Description |
---|---|
$upstream_addr |
Upstream server(s) |
$upstream_status |
Upstream status |
$upstream_response_time |
Time from upstream |
stream {
upstream db {
server 10.0.0.10:5432;
server 10.0.0.11:5432;
}
server {
listen 5432;
proxy_pass db;
}
}
stream {
server {
listen 53 udp;
proxy_responses 1;
proxy_timeout 2s;
proxy_pass 1.1.1.1:53;
}
}
stream {
server {
listen 6379;
allow 10.0.0.0/8;
deny all;
proxy_pass 127.0.0.1:6379;
}
}
# /etc/nginx/snippets/security.conf
server_tokens off;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# /etc/nginx/snippets/fastcgi-php.conf
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
# /etc/nginx/snippets/proxy-headers.conf
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;