Nginx (pronunciado "Engine-X") es uno de los servidores web más usados del mundo. Según las estadísticas de W3Techs, está presente en más del 30% de todos los sitios web. Lo encontrarás sirviendo sitios estáticos, actuando como reverse proxy delante de aplicaciones Node.js, Python o Java, gestionando certificados SSL, haciendo balanceo de carga y mucho más.
Si trabajas en backend, sistemas o DevOps, Nginx es una herramienta que vas a necesitar tarde o temprano. En esta guía aprenderás desde la instalación hasta configuraciones reales para los casos de uso más comunes: servir archivos estáticos, actuar como reverse proxy para una API, gestionar SSL/HTTPS y configurar reglas de seguridad básicas.
Qué es Nginx y en qué se diferencia de Apache
Nginx y Apache son los dos servidores web más extendidos, pero tienen arquitecturas internas muy distintas.
Apache usa un modelo basado en procesos o hilos: cada conexión entrante genera un proceso o hilo dedicado. Con muchas conexiones simultáneas, el consumo de memoria crece de forma lineal. Fue el modelo estándar durante décadas, pero tiene limitaciones a gran escala.
Nginx usa un modelo basado en eventos y un número fijo de workers (normalmente igual al número de CPUs). Cada worker puede gestionar miles de conexiones simultáneas mediante I/O no bloqueante. El resultado es un consumo de memoria mucho más bajo y un rendimiento muy superior bajo carga.
Para qué es mejor cada uno:
- Nginx: servir contenido estático, reverse proxy, balanceo de carga, alto tráfico, baja latencia.
- Apache: aplicaciones PHP tradicionales (WordPress, Drupal), entornos con
.htaccess, configuraciones por directorio.
En muchos stacks modernos se usan ambos: Nginx como reverse proxy delante de Apache o de la aplicación backend.
Instalación
# Ubuntu / Debian
sudo apt update
sudo apt install nginx
# Verificar que el servicio está corriendo
sudo systemctl status nginx
# Iniciar, detener, reiniciar
sudo systemctl start nginx
sudo systemctl stop nginx
sudo systemctl restart nginx # reinicia el proceso completo
sudo systemctl reload nginx # recarga la configuración sin reiniciar (para cambios de config)
# Habilitar inicio automático al arrancar el sistema
sudo systemctl enable nginx
# macOS con Homebrew
brew install nginx
brew services start nginx
brew services restart nginx
# Verificar la versión instalada
nginx -v
# nginx version: nginx/1.24.x
# Comprobar que la configuración es válida antes de recargar
sudo nginx -t
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successfulUna vez instalado, si visitas http://tu-ip en el navegador deberías ver la página de bienvenida de Nginx: "Welcome to nginx!".
Estructura de archivos y directorios
# Estructura principal de Nginx en Ubuntu/Debian:
/etc/nginx/
├── nginx.conf ← configuración principal (no editar directamente)
├── sites-available/ ← archivos de configuración de cada sitio
│ ├── default
│ └── mi-sitio.conf
├── sites-enabled/ ← enlaces simbólicos a sites-available (los activos)
│ └── default -> ../sites-available/default
├── conf.d/ ← configuraciones adicionales (alternativa a sites-*)
├── snippets/ ← fragmentos de configuración reutilizables
│ └── fastcgi-php.conf
└── mime.types ← mapeo de extensiones a tipos MIME
# Archivos de log
/var/log/nginx/
├── access.log ← todas las peticiones recibidas
└── error.log ← errores del servidor
# Directorio raíz del contenido web por defecto
/var/www/html/
# El flujo para activar un nuevo sitio:
# 1. Crear el archivo en sites-available/
# 2. Crear un enlace simbólico en sites-enabled/
sudo ln -s /etc/nginx/sites-available/mi-sitio.conf /etc/nginx/sites-enabled/
# 3. Verificar la configuración
sudo nginx -t
# 4. Recargar Nginx
sudo systemctl reload nginxLa configuración principal: nginx.conf
El archivo nginx.conf define el comportamiento global. Es conveniente entender su estructura aunque no lo modifiques directamente con frecuencia.
# /etc/nginx/nginx.conf
user www-data;
worker_processes auto; # un worker por CPU (auto lo detecta solo)
pid /run/nginx.pid;
events {
worker_connections 1024; # conexiones simultáneas por worker
multi_accept on; # aceptar múltiples conexiones a la vez
use epoll; # mecanismo I/O de Linux (más eficiente)
}
http {
# Tipos MIME
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Formato de logs
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
# Optimizaciones de rendimiento
sendfile on; # transferencia de archivos en modo kernel (más rápido)
tcp_nopush on; # enviar cabeceras y datos juntos
tcp_nodelay on;
keepalive_timeout 65; # mantener conexiones abiertas 65 segundos
# Compresión gzip
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 5;
gzip_types text/plain text/css application/json application/javascript
text/xml application/xml image/svg+xml;
# Ocultar versión de Nginx en cabeceras (seguridad básica)
server_tokens off;
# Incluir las configuraciones de cada sitio
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}Caso 1: Servidor de archivos estáticos
El caso de uso más simple: servir los archivos de un directorio (HTML, CSS, JS, imágenes) directamente al navegador.
# /etc/nginx/sites-available/sitio-estatico.conf
server {
listen 80;
listen [::]:80; # IPv6
server_name mi-dominio.com www.mi-dominio.com;
root /var/www/mi-sitio; # directorio raíz del contenido
index index.html index.htm; # archivo por defecto al acceder a un directorio
# Configuración de caché para archivos estáticos
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y; # caché de 1 año en el navegador
add_header Cache-Control "public, immutable";
access_log off; # no registrar peticiones de estáticos
}
# Configuración principal: servir archivos, directorio, o 404
location / {
try_files $uri $uri/ =404;
# Busca el archivo exacto → el directorio → error 404
}
# Para aplicaciones SPA (React, Vue, Angular): si el archivo no existe, sirve index.html
# location / {
# try_files $uri $uri/ /index.html;
# }
# Logs específicos de este sitio
access_log /var/log/nginx/mi-sitio-access.log;
error_log /var/log/nginx/mi-sitio-error.log;
}# Crear el directorio y un archivo de prueba
sudo mkdir -p /var/www/mi-sitio
echo "<h1>Hola desde Nginx</h1>" | sudo tee /var/www/mi-sitio/index.html
# Dar permisos correctos
sudo chown -R www-data:www-data /var/www/mi-sitio
sudo chmod -R 755 /var/www/mi-sitio
# Activar el sitio
sudo ln -s /etc/nginx/sites-available/sitio-estatico.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Caso 2: Reverse proxy para una aplicación Node.js
El caso de uso más común en aplicaciones modernas. Nginx recibe la petición, la pasa a tu aplicación Node.js (que escucha en un puerto local como 3000), recibe la respuesta y la devuelve al cliente. El cliente nunca habla directamente con Node.js.
Las ventajas de tener Nginx delante: gestiona SSL, sirve los archivos estáticos directamente sin pasar por Node, hace buffering de peticiones lentas, puede configurar rate limiting, y si tu app se cae no expone el error directamente al usuario.
# /etc/nginx/sites-available/api-node.conf
upstream backend_node {
server 127.0.0.1:3000; # tu app Node.js escucha aquí
# Para balanceo de carga entre varias instancias:
# server 127.0.0.1:3000;
# server 127.0.0.1:3001;
# server 127.0.0.1:3002;
keepalive 32; # mantener conexiones persistentes con el backend
}
server {
listen 80;
server_name api.mi-dominio.com;
# Tamaño máximo del cuerpo de la petición (útil para subida de archivos)
client_max_body_size 10M;
# Timeout de la conexión con el backend
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
location / {
proxy_pass http://backend_node;
# Cabeceras importantes para que el backend sepa la IP real del cliente
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;
# Para WebSockets (comentar si no los usas)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Buffering: Nginx recoge toda la respuesta del backend antes de enviarla al cliente
# Actívalo para clientes lentos, desactívalo para SSE/streaming
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}
# Servir archivos estáticos directamente desde Nginx (sin pasar por Node)
location /static/ {
alias /var/www/mi-app/public/;
expires 30d;
add_header Cache-Control "public";
}
access_log /var/log/nginx/api-node-access.log;
error_log /var/log/nginx/api-node-error.log;
}
En la aplicación Node.js necesitas leer la cabecera X-Forwarded-For para obtener la IP real del cliente (en lugar de la IP de Nginx):
// Node.js / Express: confiar en el proxy para obtener la IP real
const express = require('express');
const app = express();
app.set('trust proxy', 1); // confiar en el primer proxy (Nginx)
app.get('/ip', (req, res) => {
res.json({ ip: req.ip }); // ahora devuelve la IP real del cliente
});
Caso 3: HTTPS con certificado SSL de Let's Encrypt
Let's Encrypt ofrece certificados SSL gratuitos. Certbot es la herramienta oficial para obtenerlos y configurarlos automáticamente en Nginx.
# Instalar Certbot
sudo apt install certbot python3-certbot-nginx
# Obtener e instalar el certificado automáticamente
# (Certbot modifica la configuración de Nginx por ti)
sudo certbot --nginx -d mi-dominio.com -d www.mi-dominio.com
# Certbot preguntará tu email y si quieres redirigir HTTP a HTTPS
# Después de completarlo, tu sitio ya funciona con HTTPS
# Verificar renovación automática (Certbot instala un cron/timer automáticamente)
sudo certbot renew --dry-run
# Ver cuándo expiran los certificados
sudo certbot certificates
Si prefieres configurar SSL manualmente con tu propio certificado:
# /etc/nginx/sites-available/sitio-ssl.conf
# Redirigir todo el tráfico HTTP a HTTPS
server {
listen 80;
listen [::]:80;
server_name mi-dominio.com www.mi-dominio.com;
return 301 https://$host$request_uri;
}
# Servidor HTTPS
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name mi-dominio.com www.mi-dominio.com;
# Certificados (generados por Certbot en /etc/letsencrypt/)
ssl_certificate /etc/letsencrypt/live/mi-dominio.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mi-dominio.com/privkey.pem;
# Configuración SSL moderna y segura
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 1d;
ssl_session_tickets off;
# HSTS: indicar al navegador que siempre use HTTPS durante 1 año
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# OCSP Stapling: mejorar el rendimiento de la validación del certificado
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
ssl_trusted_certificate /etc/letsencrypt/live/mi-dominio.com/chain.pem;
root /var/www/mi-sitio;
index index.html;
location / {
try_files $uri $uri/ =404;
}
# Cabeceras de seguridad HTTP
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
}
Caso 4: Balanceo de carga
Cuando una sola instancia de tu aplicación no puede gestionar el tráfico, Nginx puede distribuir las peticiones entre varias instancias.
# /etc/nginx/sites-available/balanceo-carga.conf
upstream app_servers {
# Algoritmos de balanceo disponibles:
# (por defecto) round-robin: peticiones en orden circular
# least_conn: envía al servidor con menos conexiones activas
# ip_hash: misma IP siempre va al mismo servidor (útil para sesiones)
least_conn;
server 10.0.0.1:3000 weight=3; # recibe el triple de peticiones (más potente)
server 10.0.0.2:3000 weight=1;
server 10.0.0.3:3000 weight=1;
server 10.0.0.4:3000 backup; # solo se usa si los demás están caídos
# Health checks pasivos: si un servidor falla X veces, se marca como no disponible
# (requiere Nginx Plus para activos; estos son pasivos, basados en errores)
}
server {
listen 443 ssl http2;
server_name mi-app.com;
# ... configuración SSL ...
location / {
proxy_pass http://app_servers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Si un servidor falla, reintentar con el siguiente
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
proxy_next_upstream_tries 3;
}
}
Caso 5: Rate limiting y protección básica
Nginx puede limitar el número de peticiones por IP para proteger tu aplicación de ataques de fuerza bruta, scraping agresivo y DDoS básicos.
# En el bloque http de nginx.conf (o en conf.d/rate-limiting.conf)
# Definir zonas de rate limiting
# $binary_remote_addr: usa la IP del cliente en binario (menos memoria que $remote_addr)
# zone=api:10m: zona llamada "api" con 10 MB de memoria (almacena ~160.000 IPs)
# rate=10r/s: máximo 10 peticiones por segundo
http {
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
limit_conn_zone $binary_remote_addr zone=per_ip:10m;
# Código de respuesta cuando se supera el límite (429 = Too Many Requests)
limit_req_status 429;
limit_conn_status 429;
}
# Aplicar rate limiting en los locations del servidor
server {
# ...
# Rate limiting en todos los endpoints de la API
location /api/ {
limit_req zone=api burst=20 nodelay;
# burst=20: permite ráfagas de hasta 20 peticiones adicionales
# nodelay: procesa la ráfaga inmediatamente en lugar de demorarla
proxy_pass http://backend;
}
# Rate limiting más estricto en el endpoint de login (anti fuerza bruta)
location /api/auth/login {
limit_req zone=login burst=5 nodelay;
limit_conn per_ip 5; # máximo 5 conexiones simultáneas por IP
proxy_pass http://backend;
}
# Bloquear acceso a archivos sensibles
location ~ /\. {
deny all; # bloquear archivos y carpetas ocultas (.git, .env, etc.)
return 404;
}
location ~* \.(git|env|htaccess|htpasswd|ini|log|sh|sql|bak|backup|swp)$ {
deny all;
return 404;
}
# Bloquear métodos HTTP innecesarios
if ($request_method !~ ^(GET|POST|PUT|PATCH|DELETE|OPTIONS)$) {
return 405;
}
}
Variables y directivas útiles en Nginx
# Variables automáticas disponibles en la configuración
$host # nombre del servidor (del header Host)
$server_name # el server_name que coincidió
$request_uri # URI completa incluyendo query string: /ruta?param=valor
$uri # URI sin query string: /ruta
$args # query string: param=valor
$remote_addr # IP del cliente
$http_user_agent # User-Agent del cliente
$scheme # http o https
$server_port # puerto del servidor (80, 443)
$request_method # GET, POST, PUT, etc.
$status # código de respuesta HTTP
$body_bytes_sent # bytes enviados en el cuerpo de la respuesta
$request_time # tiempo total de la petición en segundos
# Ejemplos de uso en logs y condiciones
log_format detallado '$remote_addr [$time_local] "$request" $status '
'$body_bytes_sent ${request_time}s "$http_user_agent"';
# Redirigir www a no-www
server {
listen 80;
server_name www.mi-dominio.com;
return 301 $scheme://mi-dominio.com$request_uri;
}
# Redirigir una ruta antigua a una nueva
location /blog-viejo/ {
return 301 /blog/;
}
# Bloquear peticiones de ciertos User-Agents (bots)
if ($http_user_agent ~* "badbot|scraper|maliciousbot") {
return 403;
}
Logs: leer y analizar el acceso y los errores
# Ver los logs en tiempo real
sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.log
# Filtrar solo los errores 500 del log de acceso
sudo grep " 500 " /var/log/nginx/access.log | tail -20
# Las 10 IPs que más peticiones han hecho
sudo awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
# Las 10 URLs más solicitadas
sudo awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
# Peticiones por código de estado
sudo awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn
# Ver errores de los últimos 5 minutos
sudo journalctl -u nginx --since "5 minutes ago"
# Rotar los logs manualmente (normalmente logrotate lo hace automáticamente)
sudo mv /var/log/nginx/access.log /var/log/nginx/access.log.old
sudo nginx -s reopen # hace que Nginx empiece a escribir en el archivo nuevo
Configuración completa de ejemplo: API + Frontend
Un ejemplo real que combina todo: frontend React servido como estático, API Node.js como reverse proxy, HTTPS y cabeceras de seguridad.
# /etc/nginx/sites-available/mi-app-completa.conf
# Redirigir HTTP a HTTPS
server {
listen 80;
server_name mi-app.com www.mi-app.com;
return 301 https://$host$request_uri;
}
# Servidor principal HTTPS
server {
listen 443 ssl http2;
server_name mi-app.com www.mi-app.com;
# SSL
ssl_certificate /etc/letsencrypt/live/mi-app.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mi-app.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
# Seguridad
server_tokens off;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
client_max_body_size 50M;
# Frontend React (archivos estáticos del build)
root /var/www/mi-app/dist;
index index.html;
# Archivos estáticos con caché agresiva
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# SPA: todas las rutas no encontradas sirven index.html (para React Router)
location / {
try_files $uri $uri/ /index.html;
}
# API proxy: todas las peticiones a /api/ van al backend Node.js
location /api/ {
limit_req zone=api burst=30 nodelay;
proxy_pass http://127.0.0.1:3000;
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;
# Quitar /api/ del path antes de pasarlo al backend
# (si tu backend no espera el prefijo /api)
# rewrite ^/api/(.*) /$1 break;
}
# WebSockets para funcionalidades en tiempo real
location /ws/ {
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_read_timeout 3600s; # mantener la conexión WebSocket abierta 1 hora
}
# Bloquear acceso a archivos sensibles
location ~ /\. { deny all; return 404; }
access_log /var/log/nginx/mi-app-access.log;
error_log /var/log/nginx/mi-app-error.log warn;
}
Resumen: comandos de administración más usados
# Verificar la configuración antes de aplicar cambios
sudo nginx -t
# Recargar configuración sin reiniciar (sin downtime)
sudo systemctl reload nginx
# o equivalente:
sudo nginx -s reload
# Reiniciar completamente
sudo systemctl restart nginx
# Ver el estado del servicio
sudo systemctl status nginx
# Ver logs en tiempo real
sudo tail -f /var/log/nginx/error.log
# Activar un nuevo sitio
sudo ln -s /etc/nginx/sites-available/mi-sitio.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
# Desactivar un sitio
sudo rm /etc/nginx/sites-enabled/mi-sitio.conf
sudo systemctl reload nginx
# Ver todas las conexiones activas de Nginx
sudo ss -tlnp | grep nginx
# Probar la velocidad de respuesta con curl
curl -w "@-" -o /dev/null -s https://mi-dominio.com <<'EOF'
time_namelookup: %{time_namelookup}\n
time_connect: %{time_connect}\n
time_appconnect: %{time_appconnect}\n
time_pretransfer: %{time_pretransfer}\n
time_starttransfer: %{time_starttransfer}\n
time_total: %{time_total}\n
EOF
No hay comentarios todavía. Sé el primero en compartir tu opinión.