Volver al índice
root@vps-donweb:~/docs/web$

Nginx

servidor Nginx 1.x
SO Ubuntu 22.04 LTS
categoría Servidor web
SERVICIO Control de Nginx
# Iniciar, detener, reiniciar, recargar
systemctl start   nginx
systemctl stop    nginx
systemctl restart nginx   # corta conexiones activas
systemctl reload  nginx   # recarga config sin cortar conexiones

# Ver estado
systemctl status  nginx

# Habilitar/deshabilitar inicio automático
systemctl enable  nginx
systemctl disable nginx

Preferí siempre reload sobre restart en producción — no corta las conexiones activas.

CONFIG Verificar y aplicar cambios
# Testear sintaxis ANTES de recargar (obligatorio)
nginx -t

# Test con detalle
nginx -T

# Ver versión y módulos compilados
nginx -v
nginx -V

Siempre ejecutá nginx -t antes de recargar. Un error de sintaxis con restart tira el servidor.

SITES Activar y desactivar sitios
# Activar un sitio (crear symlink en sites-enabled)
ln -s /etc/nginx/sites-available/mi-sitio /etc/nginx/sites-enabled/

# Usando la herramienta incluida (si está disponible)
nginx_ensite mi-sitio

# Desactivar un sitio (borrar solo el symlink, no el archivo)
rm /etc/nginx/sites-enabled/mi-sitio

# Recargar después de activar/desactivar
nginx -t && systemctl reload nginx
LOGS Ver registros en tiempo real
# Log de accesos en vivo
tail -f /var/log/nginx/access.log

# Log de errores (el más útil para debugging)
tail -f /var/log/nginx/error.log

# Log de un sitio específico (si configuraste logs separados)
tail -f /var/log/nginx/mi-sitio.error.log

# Filtrar errores recientes
grep "error" /var/log/nginx/error.log | tail -50
ESTRUCTURA Árbol de directorios de Nginx
/etc/nginx/ ├── nginx.conf # configuración principal ├── conf.d/ # configs adicionales (se cargan automáticamente) ├── sites-available/ # configs de sitios (todos, activos o no) │ ├── default │ └── mi-sitio.conf ├── sites-enabled/ # symlinks a sites-available (solo los activos) │ └── mi-sitio.conf → ../sites-available/mi-sitio.conf ├── snippets/ # fragmentos reutilizables (SSL, fastcgi, etc.) │ ├── fastcgi-php.conf │ └── snakeoil.conf ├── mime.types # tipos MIME └── fastcgi_params # parámetros para FastCGI/PHP /var/log/nginx/ ├── access.log └── error.log /var/www/ └── html/ # directorio web por defecto

El flujo es: editar en sites-available, crear symlink en sites-enabled, recargar Nginx.

nginx.conf Estructura del archivo principal
# /etc/nginx/nginx.conf

user www-data;
worker_processes auto;           # auto = 1 worker por CPU
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 1024;
}

http {
    # Tipos MIME y charset
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    charset       utf-8;

    # Logging
    access_log /var/log/nginx/access.log;
    error_log  /var/log/nginx/error.log;

    # Performance
    sendfile        on;
    tcp_nopush      on;
    keepalive_timeout 65;
    gzip            on;

    # Cargar todos los sitios activos
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}
SITIO BÁSICO Sitio estático HTML

El bloque mínimo para servir un sitio estático. Archivo en /etc/nginx/sites-available/mi-sitio.conf

server {
    listen      80;
    listen      [::]:80;                    # IPv6
    server_name ejemplo.com www.ejemplo.com;

    root  /var/www/ejemplo.com/public;
    index index.html index.htm;

    location / {
        try_files $uri $uri/ =404;
    }

    # Logs separados por sitio (recomendado)
    access_log /var/log/nginx/ejemplo.com.access.log;
    error_log  /var/log/nginx/ejemplo.com.error.log;
}
# Activar y recargar
ln -s /etc/nginx/sites-available/mi-sitio.conf /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
SUBDOMINIO Configuración de subdominio independiente

Cada subdominio es un bloque server separado con su propio server_name. Archivo en /etc/nginx/sites-available/app.ejemplo.com.conf

server {
    listen      80;
    listen      [::]:80;
    server_name app.ejemplo.com;

    root  /var/www/app.ejemplo.com/public;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    access_log /var/log/nginx/app.ejemplo.com.access.log;
    error_log  /var/log/nginx/app.ejemplo.com.error.log;
}

El DNS del subdominio debe apuntar a la IP del VPS con un registro tipo A antes de que funcione.

WILDCARD Múltiples subdominios en un bloque

Útil para capturar cualquier subdominio con la misma lógica.

server {
    listen      80;
    server_name ~^(?<sub>.+)\.ejemplo\.com$;   # regex

    root  /var/www/$sub/public;   # carpeta según subdominio
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

Con wildcard DNS (registro *.ejemplo.com en el DNS) y este bloque, cada subdominio sirve desde su propia carpeta automáticamente.

REDIRECT Redireccionamientos comunes
# www → sin www (301 permanente)
server {
    listen      80;
    server_name www.ejemplo.com;
    return 301 https://ejemplo.com$request_uri;
}

# HTTP → HTTPS
server {
    listen      80;
    server_name ejemplo.com www.ejemplo.com;
    return 301 https://$host$request_uri;
}

# Redirigir una ruta específica
location /vieja-ruta {
    return 301 /nueva-ruta;
}

# Redirigir con expresión regular
rewrite ^/blog/(.*)$ /articulos/$1 permanent;
HTTPS Sitio con SSL (Let's Encrypt / Certbot)

Configuración completa HTTPS. Certbot puede generarla automáticamente, pero es útil entender su estructura.

# Bloque HTTP → redirige a HTTPS
server {
    listen 80;
    server_name ejemplo.com www.ejemplo.com;
    return 301 https://$host$request_uri;
}

# Bloque HTTPS principal
server {
    listen      443 ssl http2;
    listen      [::]:443 ssl http2;
    server_name ejemplo.com www.ejemplo.com;

    # Certificados (generados por Certbot)
    ssl_certificate     /etc/letsencrypt/live/ejemplo.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ejemplo.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    root  /var/www/ejemplo.com/public;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

Con Certbot: certbot --nginx -d ejemplo.com -d www.ejemplo.com configura todo esto automáticamente.

PHP-FPM Cómo funciona con Nginx

Nginx no ejecuta PHP directamente. Pasa las peticiones PHP al proceso PHP-FPM (FastCGI Process Manager) vía socket Unix o TCP. PHP-FPM procesa y devuelve la respuesta a Nginx.

# Instalar PHP-FPM (reemplazar 8.2 por tu versión)
apt install php8.2-fpm php8.2-mysql php8.2-curl php8.2-mbstring

# Ver estado de PHP-FPM
systemctl status php8.2-fpm

# Ver versión instalada
php -v

# Verificar el socket (la ruta que va en nginx)
ls /run/php/
# → php8.2-fpm.sock
CONFIG Sitio PHP genérico
server {
    listen      80;
    server_name mi-php-app.com;
    root        /var/www/mi-php-app/public;
    index       index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # Pasar archivos .php a PHP-FPM
    location ~ \.php$ {
        include         snippets/fastcgi-php.conf;
        fastcgi_pass    unix:/run/php/php8.2-fpm.sock;
        fastcgi_param   SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include         fastcgi_params;
    }

    # Bloquear acceso a archivos .htaccess (no aplican en Nginx)
    location ~ /\.ht {
        deny all;
    }

    access_log /var/log/nginx/mi-php-app.access.log;
    error_log  /var/log/nginx/mi-php-app.error.log;
}
LARAVEL Configuración para Laravel / Symfony

Frameworks PHP modernos requieren que todas las peticiones pasen por index.php (front controller). El root apunta a la carpeta public/.

server {
    listen      80;
    server_name laravel.ejemplo.com;
    root        /var/www/laravel/public;   # ← siempre /public
    index       index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass    unix:/run/php/php8.2-fpm.sock;
        fastcgi_index   index.php;
        fastcgi_param   SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include         fastcgi_params;
        fastcgi_hide_header X-Powered-By;
    }

    # Archivos estáticos con caché larga
    location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff2)$ {
        expires    30d;
        add_header Cache-Control "public, no-transform";
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }

    access_log /var/log/nginx/laravel.access.log;
    error_log  /var/log/nginx/laravel.error.log;
}
WORDPRESS Configuración para WordPress
server {
    listen      80;
    server_name mi-wp.com www.mi-wp.com;
    root        /var/www/mi-wp;
    index       index.php;

    # Permalinks de WordPress
    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    # PHP-FPM
    location ~ \.php$ {
        fastcgi_pass   unix:/run/php/php8.2-fpm.sock;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }

    # Bloquear acceso a wp-config y archivos sensibles
    location ~* /(wp-config\.php|\.git|readme\.html|license\.txt) {
        deny all;
    }

    # Caché de estáticos
    location ~* \.(css|js|jpg|jpeg|png|gif|ico|woff2|svg)$ {
        expires    1y;
        add_header Cache-Control "public";
        log_not_found off;
    }

    # Límite de tamaño de subida (sincronizar con php.ini)
    client_max_body_size 64M;
}
PHP-FPM POOLS Pools separados por sitio

Tener un pool de PHP-FPM por sitio aísla los procesos y mejora la seguridad y el monitoreo. Archivo en /etc/php/8.2/fpm/pool.d/mi-sitio.conf

; Pool para mi-sitio (copia de www.conf y adaptá)
[mi-sitio]
user  = www-data
group = www-data

; Socket exclusivo para este sitio
listen = /run/php/mi-sitio.sock

pm                   = dynamic
pm.max_children      = 10
pm.start_servers     = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
# En nginx, usar el socket del pool específico
fastcgi_pass unix:/run/php/mi-sitio.sock;
PROXY Proxy inverso básico

Nginx recibe la petición del cliente y la reenvía a una aplicación corriendo en otro puerto (Node.js, Python, Go, Java, etc.).

server {
    listen      80;
    server_name api.ejemplo.com;

    location / {
        proxy_pass         http://127.0.0.1:3000;  # app en puerto 3000
        proxy_http_version 1.1;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection 'upgrade';
        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_cache_bypass $http_upgrade;
    }
}

Los headers X-Real-IP y X-Forwarded-For permiten que tu app vea la IP real del cliente, no la de Nginx.

UPSTREAM Balanceo de carga (múltiples instancias)
# Definir el grupo de servidores
upstream mi_app {
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
    keepalive 32;    # conexiones persistentes al upstream
}

server {
    listen      80;
    server_name api.ejemplo.com;

    location / {
        proxy_pass       http://mi_app;    # usa el upstream
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Por defecto usa round-robin. Podés agregar least_conn; dentro del upstream para enviar al servidor con menos conexiones activas.

RUTAS Proxy por ruta (split de API y frontend)

Un mismo dominio sirve el frontend estático y redirige las rutas de la API a la app.

server {
    listen      80;
    server_name ejemplo.com;

    # API → app en Node/Python
    location /api/ {
        proxy_pass       http://127.0.0.1:4000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # WebSockets
    location /ws/ {
        proxy_pass         http://127.0.0.1:4000;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection "Upgrade";
    }

    # Frontend → archivos estáticos
    location / {
        root      /var/www/ejemplo.com/dist;
        try_files $uri $uri/ /index.html;   # SPA fallback
    }
}
PYTHON Django con Gunicorn

Gunicorn es el servidor WSGI que corre la app Django. Nginx actúa como proxy inverso delante de él.

# 1. Instalar Gunicorn en el entorno virtual
pip install gunicorn

# 2. Iniciar Gunicorn manualmente (para probar)
gunicorn --workers 3 --bind 127.0.0.1:8000 mi_proyecto.wsgi:application

# 3. Crear servicio systemd para que arranque solo
# /etc/systemd/system/gunicorn.service
# /etc/systemd/system/gunicorn.service
[Unit]
Description=Gunicorn daemon para Django
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/mi-django
ExecStart=/var/www/mi-django/venv/bin/gunicorn \
    --workers 3 \
    --bind unix:/run/gunicorn/mi-django.sock \
    mi_proyecto.wsgi:application

[Install]
WantedBy=multi-user.target
systemctl enable gunicorn
systemctl start  gunicorn
# Nginx → Gunicorn (via socket Unix)
server {
    listen      80;
    server_name mi-django.com;

    # Archivos estáticos servidos directamente por Nginx
    location /static/ {
        alias /var/www/mi-django/staticfiles/;
    }

    location /media/ {
        alias /var/www/mi-django/media/;
    }

    # Todo lo demás → Gunicorn
    location / {
        proxy_pass         http://unix:/run/gunicorn/mi-django.sock;
        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;
    }
}
FLASK Flask / FastAPI con Uvicorn

FastAPI y apps ASGI usan Uvicorn (o Hypercorn). El patrón es idéntico al de Gunicorn.

# Instalar
pip install uvicorn gunicorn

# Para FastAPI con Uvicorn workers
gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app --bind 127.0.0.1:8001

# Para Flask con Gunicorn
gunicorn -w 4 "app:create_app()" --bind 127.0.0.1:8001
# Nginx → FastAPI / Flask
server {
    listen      80;
    server_name api.ejemplo.com;

    location / {
        proxy_pass         http://127.0.0.1:8001;
        proxy_http_version 1.1;
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-Proto $scheme;
    }
}
NODE.JS Node.js / Next.js con PM2

PM2 mantiene el proceso Node vivo y lo reinicia si falla. Nginx hace el proxy adelante.

# Instalar PM2 globalmente
npm install -g pm2

# Iniciar la app
pm2 start server.js --name "mi-app"

# Para Next.js
pm2 start npm --name "nextjs" -- start

# Guardar configuración y activar al inicio
pm2 save
pm2 startup

# Comandos útiles de PM2
pm2 list            # ver apps corriendo
pm2 logs mi-app     # ver logs
pm2 restart mi-app  # reiniciar
pm2 stop mi-app     # detener
pm2 monit           # monitor interactivo
# Nginx → Node.js / Next.js
server {
    listen      80;
    server_name mi-node-app.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_set_header   X-Real-IP $remote_addr;
        proxy_read_timeout 300s;
        proxy_cache_bypass $http_upgrade;
    }
}
DOCKER Nginx como proxy para contenedores

Cuando la app corre en Docker, Nginx proxy al puerto mapeado del contenedor.

# El contenedor expone su puerto al host
# docker run -p 127.0.0.1:8080:80 mi-app
# ↑ solo en loopback, Nginx hace el proxy público

server {
    listen      80;
    server_name docker-app.ejemplo.com;

    location / {
        proxy_pass       http://127.0.0.1:8080;  # puerto del contenedor
        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;
    }
}

Mapear el contenedor a 127.0.0.1:puerto (no solo :puerto) evita que el puerto quede expuesto públicamente; solo Nginx puede accederlo.

HEADERS Headers de seguridad HTTP

Agregar estos headers en el bloque server o en nginx.conf dentro de http {} para que apliquen a todos los sitios.

# Ocultar versión de Nginx
server_tokens off;

# Protección contra clickjacking
add_header X-Frame-Options "SAMEORIGIN" always;

# Protección XSS
add_header X-XSS-Protection "1; mode=block" always;

# Evitar sniffing de MIME
add_header X-Content-Type-Options "nosniff" always;

# HTTPS estricto (solo en sitios con SSL)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

# Política de referrer
add_header Referrer-Policy "no-referrer-when-downgrade" always;

# Content Security Policy (adaptar a tu app)
add_header Content-Security-Policy "default-src 'self'; script-src 'self'" always;
RATE LIMIT Limitar peticiones por IP
# Definir zona de límite en http{} de nginx.conf
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;

# Aplicar en el server block
location /api/ {
    limit_req zone=api_limit burst=20 nodelay;
    proxy_pass http://127.0.0.1:3000;
}

# Más estricto para el login
location /login {
    limit_req zone=login_limit burst=3;
    proxy_pass http://127.0.0.1:3000;
}

burst permite picos temporales. nodelay procesa el burst inmediatamente en vez de poner en cola.

ACCESO Restricciones por IP y autenticación básica
# Permitir solo ciertas IPs (ej: panel de admin)
location /admin {
    allow  192.168.1.100;   # tu IP
    allow  10.0.0.0/8;      # red interna
    deny   all;
    proxy_pass http://127.0.0.1:3000;
}

# Autenticación HTTP básica
location /privado {
    auth_basic           "Área restringida";
    auth_basic_user_file /etc/nginx/.htpasswd;
}
# Crear archivo de contraseñas
apt install apache2-utils
htpasswd -c /etc/nginx/.htpasswd mi_usuario
BLOQUEOS Bloquear bots y rutas sensibles
# Bloquear user-agents maliciosos
map $http_user_agent $blocked_agent {
    default    0;
    ~*malicious 1;
    ~*scanner   1;
    ~*harvester 1;
}

server {
    if ($blocked_agent) {
        return 403;
    }

    # Ocultar archivos ocultos (.env, .git, etc.)
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }

    # Bloquear acceso a archivos de configuración
    location ~* \.(env|log|conf|ini|sh|sql|bak)$ {
        deny all;
    }
}
GZIP Compresión de respuestas
# En el bloque http{} de nginx.conf
gzip              on;
gzip_vary         on;
gzip_proxied      any;
gzip_comp_level   6;            # 1-9, balance CPU/compresión
gzip_min_length   1024;         # no comprimir respuestas pequeñas
gzip_buffers      16 8k;
gzip_http_version 1.1;
gzip_types
    text/plain
    text/css
    text/javascript
    application/javascript
    application/json
    application/xml
    image/svg+xml
    font/woff2;
CACHÉ Cache de archivos estáticos
# Caché larga para assets con hash en el nombre
location ~* \.(css|js|woff2|woff|ttf)$ {
    expires    1y;
    add_header Cache-Control "public, immutable";
}

# Caché media para imágenes
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp)$ {
    expires    30d;
    add_header Cache-Control "public, no-transform";
    log_not_found off;
}

# Sin caché para HTML y JSON (contenido dinámico)
location ~* \.(html|htm)$ {
    expires    -1;
    add_header Cache-Control "no-cache, no-store, must-revalidate";
}
TIMEOUTS Timeouts y tamaños de buffer
# En http{} de nginx.conf

# Timeouts de conexión
client_body_timeout   30s;
client_header_timeout 30s;
keepalive_timeout     65s;
send_timeout          30s;

# Límite de tamaño de body (subida de archivos)
client_max_body_size  32M;

# Buffers
client_body_buffer_size   128k;
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;

# Proxy timeouts (para apps lentas)
proxy_connect_timeout 60s;
proxy_send_timeout    60s;
proxy_read_timeout    60s;
CACHÉ PROXY Cache de respuestas del upstream
# Definir zona de caché en http{}
proxy_cache_path /var/cache/nginx
    levels=1:2
    keys_zone=mi_cache:10m
    max_size=1g
    inactive=60m
    use_temp_path=off;

# Usar caché en el server block
location /api/ {
    proxy_cache            mi_cache;
    proxy_cache_valid      200 10m;    # cachear 200 por 10 min
    proxy_cache_valid      404 1m;
    proxy_cache_use_stale  error timeout updating;
    proxy_cache_lock       on;
    add_header             X-Cache-Status $upstream_cache_status;
    proxy_pass             http://127.0.0.1:3000;
}

El header X-Cache-Status muestra si la respuesta fue HIT, MISS o BYPASS — útil para debugging.

SNIPPET Fragmento reutilizable para proxy

Guardá configuración repetida en un snippet e incluilo desde cualquier server block. Archivo en /etc/nginx/snippets/proxy-params.conf

# /etc/nginx/snippets/proxy-params.conf
proxy_http_version  1.1;
proxy_cache_bypass  $http_upgrade;
proxy_set_header    Upgrade $http_upgrade;
proxy_set_header    Connection 'upgrade';
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;
# Usarlo en cualquier location
location / {
    include    snippets/proxy-params.conf;
    proxy_pass http://127.0.0.1:3000;
}