If your web server crashes at the first traffic spike, or if pages load at a glacial pace, it's not the code. It's the web server configuration. We at Meteora Web have seen dozens of projects arrive with sluggish Apache, unoptimized PHP-FPM, and Nginx running with defaults. Result: awful response times, wasted resources, and lost customers. In this pillar page we give you everything you need to turn Nginx into a production-ready missile. No theory, only commands and decisions you can apply today.
Why Nginx is the starting point for every modern application
Nginx isn't just a web server. It's a reverse proxy, load balancer, static content accelerator, and API gateway, all in one. Its event-driven architecture makes it lighter and more scalable than Apache on the same hardware. We use it as the front-end for every project: whether it's Laravel, WordPress, or a Vue SPA, Nginx sits in front and handles traffic.
The concrete benefit: with Nginx you can serve thousands of simultaneous connections with very little RAM. Worker processes are configurable based on CPU cores, keepalive queues reduce three-way handshakes, and static file caching offloads PHP or Python. In short: you spend less on servers and get better performance.
Sponsored Protocol
Installation and basic configuration
On Ubuntu (our go‑to distribution) just two commands:
sudo apt update
sudo apt install nginx
Then enable and start:
sudo systemctl enable nginx
sudo systemctl start nginx
curl localhost
If you see the Nginx welcome page, you're set. Then create a virtual host for your domain.
Virtual host structure
In Nginx, each domain has a file in /etc/nginx/sites-available/, enabled via a symlink in /etc/nginx/sites-enabled/. A minimal static site:
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
root /var/www/yourdomain;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
After each change, always run sudo nginx -t and then sudo systemctl reload nginx.
Nginx as a reverse proxy
This is where Nginx shines. All modern apps (PHP-FPM, Node.js, Python) run on internal ports. Nginx exposes port 80/443 and forwards requests. This:
- Protects the application from direct attacks
- Centralizes SSL, security headers, and limits
- Places static cache in front of everything
Reverse proxy for PHP-FPM (WordPress, Laravel)
server {
listen 80;
server_name my-site.com;
root /var/www/my-site/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location ~ /\.ht {
deny all;
}
}
Note the Unix socket – faster than TCP. On shared servers or containers, use TCP (127.0.0.1:9000).
Sponsored Protocol
Reverse proxy for Node.js (Express, Nuxt, Next)
server {
listen 80;
server_name api.my-site.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
The proxy_http_version 1.1 and upgrade headers are essential for WebSockets.
SSL/TLS with Let's Encrypt and auto‑renewal
We dropped paid certificates the moment Let's Encrypt made TLS free and automated. Setup:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
Certbot modifies your server blocks and sets up HTTP→HTTPS redirect. Then enable automatic renewal:
Sponsored Protocol
sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer
Always check: sudo systemctl list-timers | grep certbot.
Strong TLS 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;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
Test your domain at SSL Labs.
Performance tuning: workers, cache, keepalive
Worker processes and connections
worker_processes auto;
events {
worker_connections 1024;
}
auto sets workers equal to CPU cores. Raise worker_connections to 1024 from default 512.
Keepalive and buffers
http {
keepalive_timeout 65;
keepalive_requests 100;
client_body_buffer_size 128k;
client_max_body_size 10m;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
}
Static cache
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
Load balancing with Nginx
upstream backend {
server 10.0.0.1:3000 weight=3;
server 10.0.0.2:3000;
server 10.0.0.3:3000 backup;
}
server {
listen 80;
server_name loadbalanced.com;
location / {
proxy_pass http://backend;
}
}
- Round Robin (default)
- Least Connections:
least_conn; - IP Hash:
ip_hash;
For more advanced scenarios, check our article on API Gateway deployment patterns.
Sponsored Protocol
Security headers and basic protection
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy strict-origin-when-cross-origin;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()";
Rate limiting for API protection
http {
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
server {
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://backend;
}
}
}
Nginx with Docker
version: '3'
services:
nginx:
image: nginx:stable-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
- ./html:/usr/share/nginx/html:ro
depends_on:
- php
- node
php:
image: php:8.3-fpm-alpine
volumes:
- ./html:/usr/share/nginx/html
Access log and error log: parsing and monitoring
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" $request_time';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
}
Send logs to a tool like Grafana for alerts. See our Grafana guide.
Sponsored Protocol
Nginx vs Apache vs Caddy
- Apache: useful for embedded PHP (
mod_php), but lags in performance and concurrency. - Caddy: easy setup with automatic HTTPS, but smaller ecosystem. Good for tiny projects.
- Nginx: the best balance of flexibility, performance, and community. Our default choice.
In summary – what to do now
- Install Nginx on a test server and create a virtual host with free SSL.
- Set up reverse proxy for your app (PHP, Node, Python).
- Apply performance tuning (workers, keepalive, static cache).
- Configure rate limiting and security headers on at least one public endpoint.
- Set up access log monitoring.
We use these same techniques every day for clients across Italy. If you'd like a second pair of eyes on your server, get in touch.