Qué es Nginx y cómo configurarlo como servidor web o reverse proxy

D
DanisCh
(Actualizado: ) • 16 min de lectura
Qué es Nginx y cómo configurarlo como servidor web o reverse proxy
Hosting y VPS DevOps

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 successful

Una 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 nginx

La 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
Etiquetas: Nginx DevOps

¿Te ha gustado esta entrada?

Compártela con tus compañeros para que también sigan aprendiendo.

Comunidad y Comentarios

0 COMENTARIOS

No hay comentarios todavía. Sé el primero en compartir tu opinión.

Escribe tu opinión
Respondiendo a