Nginx: Curso Intensivo de Cero a Experto

Nginx: Curso Intensivo de Cero a Experto

Aprende Nginx desde cero: cómo funciona internamente, proxy inverso, load balancer, SSL, caché, compresión y optimización completa del frontend.

Por Omar Flores

Imagina una ciudad de un millón de habitantes con un solo cruce de calles sin semáforos. Cada auto, moto y peatón llega al mismo punto al mismo tiempo. No hay carriles, no hay turnos, no hay prioridades. El resultado no es caos — es parálisis total. Nada se mueve.

Ahora imagina que alguien instala un sistema de semáforos inteligentes, distribuidores viales, carriles exclusivos y pasos a desnivel. El mismo volumen de tráfico fluye sin fricciones porque hay algo en el centro que entiende la red completa y sabe cómo dirigirla.

Eso es Nginx en un sistema de software. No es solo un servidor web. Es el punto de control que separa el internet caótico de tus aplicaciones ordenadas.

El problema que Nginx resuelve

Cuando un equipo despliega una aplicación por primera vez, el flujo es simple: el navegador habla directamente con el proceso de la aplicación. Funciona. Pero solo hasta que llegan más usuarios, más servicios, más dominios, certificados SSL, assets estáticos, y la necesidad de que nada se caiga mientras se despliega una nueva versión.

He visto equipos pequeños colapsar bajo ese peso. No porque su código fuera malo, sino porque no tenían una capa de infraestructura que absorbiera la complejidad entre el exterior y sus servicios. Cada problema nuevo se resolvía con un parche directamente en la aplicación: lógica de caché en el código, redirecciones HTTP hardcodeadas, headers de seguridad duplicados en cada microservicio.

Nginx externaliza toda esa responsabilidad. Lo que antes vivía disperso en cinco servicios vive en un solo archivo de configuración bajo control de versiones, legible, predecible y fácil de auditar.

Cómo funciona Nginx internamente

La mayoría de los servidores web tradicionales usaban un modelo de proceso por conexión. Por cada usuario que llegaba, se creaba un hilo del sistema operativo. A 10,000 conexiones simultáneas, tienes 10,000 hilos. Cada hilo consume memoria. El sistema operativo pasa más tiempo cambiando de contexto entre hilos que procesando peticiones reales. Este era el problema C10K — 10,000 conexiones simultáneas rompían los servidores de la época.

Nginx lo resolvió con una arquitectura diferente: un proceso maestro que administra un número fijo de procesos trabajadores. Cada trabajador maneja miles de conexiones simultáneas usando un bucle de eventos no bloqueante. En lugar de crear un hilo por conexión, Nginx registra cada conexión como un evento y la procesa cuando hay datos disponibles, sin bloquear mientras espera.

Proceso Maestro (PID 1)
├── Worker 1  →  maneja miles de conexiones via event loop
├── Worker 2  →  maneja miles de conexiones via event loop
├── Worker 3  →  maneja miles de conexiones via event loop
└── Worker 4  →  maneja miles de conexiones via event loop

El número de workers suele configurarse igual al número de núcleos de CPU disponibles. El proceso maestro no maneja tráfico — solo administra los workers, lee la configuración y maneja señales del sistema operativo.

Esta arquitectura explica por qué Nginx puede servir decenas de miles de conexiones simultáneas con memoria y CPU mínimos.

Instalación y estructura de archivos

En sistemas basados en Debian o Ubuntu, la instalación es directa:

sudo apt update
sudo apt install nginx

# Verificar que el servicio está corriendo
sudo systemctl status nginx
sudo systemctl enable nginx  # Iniciar automáticamente al arrancar

Una vez instalado, la estructura de archivos que encontrarás:

/etc/nginx/
├── nginx.conf              # Configuración principal
├── conf.d/                 # Archivos de configuración adicionales
├── sites-available/        # Configuraciones de sitios (desactivadas por default)
└── sites-enabled/          # Symlinks a sites-available (las activas)

/var/log/nginx/
├── access.log              # Cada petición que llega
└── error.log               # Errores del servidor

/var/www/html/              # Raíz web por defecto

La convención sites-available / sites-enabled es de Debian. En otras distribuciones y en configuraciones de producción modernas, se usa directamente conf.d/. Ambas funcionan igual — sites-enabled solo contiene symlinks a sites-available.

La anatomía de nginx.conf

Antes de tocar cualquier configuración de sitio, es necesario entender cómo Nginx estructura su configuración. Todo opera en bloques jerárquicos:

# Nivel global — aplica al proceso completo
worker_processes auto;
error_log /var/log/nginx/error.log warn;

events {
    # Configuración del event loop
    worker_connections 1024;
}

http {
    # Configuración del protocolo HTTP — aplica a todos los sitios
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    server {
        # Un servidor virtual — un dominio o IP
        listen 80;
        server_name ejemplo.com;

        location / {
            # Reglas para rutas específicas
            root /var/www/html;
            index index.html;
        }
    }
}

El bloque events controla cómo maneja las conexiones. El bloque http agrupa todo lo relacionado con HTTP. Dentro de http, cada bloque server es un virtual host — puede tener múltiples server blocks, uno por dominio. Dentro de cada server, los bloques location definen qué hacer con cada ruta.

La herencia funciona de afuera hacia adentro: lo definido en http aplica a todos los server blocks, y lo definido en server aplica a todos sus location blocks, a menos que el bloque interior lo sobreescriba.

Server blocks: virtual hosting

El virtual hosting permite que un solo servidor físico sirva múltiples dominios. Nginx elige qué server block activar basándose en el header Host de la petición HTTP.

# Sitio principal
server {
    listen 80;
    server_name sazardev.com www.sazardev.com;
    root /var/www/sazardev;
    index index.html;
}

# API en subdominio
server {
    listen 80;
    server_name api.sazardev.com;
    root /var/www/api;
}

# Sitio de staging
server {
    listen 80;
    server_name staging.sazardev.com;
    root /var/www/staging;
}

Cuando llega una petición a api.sazardev.com, Nginx lee el header Host, encuentra el server block cuyo server_name coincide, y aplica su configuración. Si no hay coincidencia, Nginx usa el primer server block o el marcado como default_server.

Para marcar un bloque como default explícito:

server {
    listen 80 default_server;
    server_name _;  # _ es un nombre inválido que nunca coincide con nada real
    return 444;     # Cierra la conexión sin respuesta — útil para rechazar tráfico no esperado
}

Proxy inverso: el uso más importante

Un proxy inverso recibe peticiones del exterior y las reenvía a un servicio interno. El cliente nunca habla directamente con tu aplicación. Nginx actúa como intermediario.

Este patrón es el corazón de casi toda infraestructura moderna. Tu aplicación Node.js, Python, Go o Java corre en un puerto interno (3000, 8080, etc.) sin estar expuesta directamente al internet.

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

    location / {
        proxy_pass http://127.0.0.1:3000;

        # Headers necesarios para que la app sepa el origen real de la petición
        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;
    }
}

Los headers proxy_set_header son críticos. Sin ellos, tu aplicación recibiría 127.0.0.1 como IP del cliente en todos los logs y en toda lógica de rate limiting o geolocalización. Con X-Real-IP y X-Forwarded-For, la aplicación recibe la IP real del usuario.

X-Forwarded-Proto le dice a la aplicación si la conexión original era HTTP o HTTPS — importante cuando la app necesita generar URLs absolutas o aplicar redirecciones.

Para aplicaciones con WebSockets, necesitas configuración adicional para que la conexión se pueda actualizar de HTTP a WebSocket:

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";
}

Load balancing: distribuir carga entre instancias

Cuando una sola instancia de tu aplicación no es suficiente, Nginx puede distribuir el tráfico entre múltiples instancias. El bloque upstream define el grupo de servidores:

upstream backend_pool {
    # Round-robin por defecto: petición 1 → server1, petición 2 → server2, etc.
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
    server 127.0.0.1:3003;
}

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

    location / {
        proxy_pass http://backend_pool;
    }
}

Nginx ofrece varios algoritmos de balanceo. Round-robin es el default — cada servidor recibe una petición en turno. Para aplicaciones donde algunas peticiones son más pesadas que otras, least_conn envía cada nueva petición al servidor con menos conexiones activas en ese momento:

upstream backend_pool {
    least_conn;
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
    server 127.0.0.1:3003;
}

Para aplicaciones con sesiones que no usan tokens stateless, ip_hash garantiza que el mismo cliente siempre llega al mismo servidor:

upstream backend_pool {
    ip_hash;
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
}

Si un servidor falla, Nginx lo marca como no disponible y deja de enviarle tráfico automáticamente. El parámetro weight permite darle más tráfico a instancias más potentes:

upstream backend_pool {
    server 10.0.0.1:3000 weight=3;   # recibe 3 de cada 4 peticiones
    server 10.0.0.2:3000 weight=1;   # recibe 1 de cada 4 peticiones
    server 10.0.0.3:3000 backup;     # solo activo si los demás fallan
}

SSL/TLS: HTTPS desde cero

HTTP sin cifrado expone todo el tráfico en texto plano. Cualquier nodo en la red entre el cliente y el servidor puede leer contraseñas, tokens y datos sensibles. HTTPS cifra el canal completo.

Con Certbot y Let’s Encrypt puedes obtener un certificado gratuito en minutos:

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d ejemplo.com -d www.ejemplo.com

Certbot modifica automáticamente tu configuración de Nginx para activar HTTPS. Lo que hace manualmente se ve así:

server {
    listen 443 ssl;
    server_name ejemplo.com www.ejemplo.com;

    ssl_certificate     /etc/letsencrypt/live/ejemplo.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ejemplo.com/privkey.pem;

    # Protocolos modernos — deshabilita SSL 3.0, TLS 1.0 y 1.1
    ssl_protocols TLSv1.2 TLSv1.3;

    # Ciphers seguros — el orden importa
    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;

    # Session cache — evita repetir el handshake completo en cada petición
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;

    # HSTS — le dice al navegador que nunca use HTTP para este dominio
    add_header Strict-Transport-Security "max-age=63072000" always;
}

# Redirigir todo el tráfico HTTP a HTTPS
server {
    listen 80;
    server_name ejemplo.com www.ejemplo.com;
    return 301 https://$host$request_uri;
}

La directiva ssl_session_cache es importante para el rendimiento. El handshake TLS es costoso computacionalmente. Con session cache, las conexiones subsiguientes del mismo cliente reutilizan los parámetros ya negociados, reduciendo la latencia significativamente.

Optimización del frontend: compresión y caché

Aquí es donde Nginx tiene el mayor impacto en el rendimiento percibido por el usuario.

Compresión gzip y Brotli

Comprimir las respuestas reduce el tamaño de transferencia entre 60% y 80% para HTML, CSS y JavaScript. El servidor gasta CPU comprimiendo; el cliente gasta CPU descomprimiendo. Para la mayoría de las redes, este trade-off siempre favorece la compresión.

http {
    gzip on;
    gzip_vary on;          # Agrega header Vary: Accept-Encoding
    gzip_proxied any;      # Comprimir también respuestas de proxies upstream
    gzip_comp_level 6;     # Nivel 1-9. 6 es el punto óptimo velocidad/compresión
    gzip_min_length 1000;  # No comprimir respuestas menores a 1KB — overhead no vale la pena
    gzip_types
        text/plain
        text/css
        text/javascript
        application/javascript
        application/json
        application/xml
        image/svg+xml
        font/ttf
        font/woff
        font/woff2;
}

gzip_vary on agrega el header Vary: Accept-Encoding a las respuestas. Sin esto, un CDN o proxy intermedio podría cachear la versión comprimida y servirla a un cliente que no soporta gzip.

Cache-Control para assets estáticos

Los navegadores y CDNs respetan el header Cache-Control. La estrategia correcta: assets con hash en el nombre de archivo (como genera Vite, Webpack o Astro) pueden cachearse indefinidamente porque si el contenido cambia, el nombre cambia. Los archivos sin hash necesitan tiempos más cortos.

server {
    # Assets con hash en el nombre: cachear 1 año
    location ~* \.(js|css|woff2|woff|ttf)$ {
        expires 1y;
        add_header Cache-Control "public, max-age=31536000, immutable";
        access_log off;  # No necesitamos log de cada asset — reduce I/O
    }

    # Imágenes: cachear 30 días
    location ~* \.(jpg|jpeg|png|webp|avif|ico|svg)$ {
        expires 30d;
        add_header Cache-Control "public, max-age=2592000";
        access_log off;
    }

    # HTML: nunca cachear — siempre buscar la versión más reciente
    location ~* \.html$ {
        expires -1;
        add_header Cache-Control "no-cache, no-store, must-revalidate";
    }
}

El valor immutable en Cache-Control le dice al navegador que no intente revalidar el archivo ni siquiera cuando el usuario hace refresh. Es seguro usarlo solo en archivos con hash, porque si el contenido cambia, el nombre también cambia.

Servir assets pre-comprimidos

Si tu proceso de build genera archivos .gz o .br (Brotli), Nginx puede servirlos directamente sin comprimir en tiempo real. Esto elimina el costo de CPU de la compresión en cada petición:

location ~* \.(js|css)$ {
    gzip_static on;   # Busca archivo.js.gz antes de comprimir archivo.js
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable";
}

Cuando un cliente pide app.js y su Accept-Encoding incluye gzip, Nginx sirve app.js.gz directamente si existe. Sin CPU adicional, sin latencia de compresión.

Headers de seguridad

Los headers de seguridad son una capa de protección que no requiere cambios en el código de la aplicación. Se configuran una vez en Nginx y aplican a todas las respuestas.

server {
    # Previene que el navegador adivine el tipo de contenido (MIME sniffing)
    add_header X-Content-Type-Options "nosniff" always;

    # Controla qué información se envía en el header Referer
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Política de permisos — deshabilita APIs del navegador que no necesitas
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

    # Content Security Policy — define qué recursos puede cargar la página
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;" always;

    # Ocultar la versión de Nginx — no revelar información al atacante
    server_tokens off;
}

server_tokens off es una directiva simple pero importante. Sin ella, Nginx incluye su número de versión en headers de error y en el header Server. Un atacante que conoce la versión exacta puede buscar CVEs específicos para esa versión.

El header Content-Security-Policy es el más poderoso y el más complejo. La política default-src 'self' bloquea cualquier recurso externo por defecto — scripts, estilos, imágenes, fuentes, iframes. Cada excepción debe declararse explícitamente. Para sitios que usan fuentes de Google o CDNs externos, necesitarás ampliar la política.

Caché de proxy

Nginx puede cachear las respuestas de tu aplicación en disco. Cuando la misma URL se pide múltiples veces en un período corto, Nginx responde desde caché sin llegar a la aplicación. Para endpoints de contenido estático o semi-estático, esto puede reducir la carga del servidor de aplicaciones en un 90%.

http {
    # Definir la zona de caché: 10MB de metadatos en RAM, archivos en /var/cache/nginx
    proxy_cache_path /var/cache/nginx
                     levels=1:2
                     keys_zone=app_cache:10m
                     max_size=1g
                     inactive=60m
                     use_temp_path=off;

    server {
        location / {
            proxy_pass http://127.0.0.1:3000;

            proxy_cache            app_cache;
            proxy_cache_valid      200 10m;   # Cachear respuestas 200 por 10 minutos
            proxy_cache_valid      404 1m;    # Cachear 404 por 1 minuto
            proxy_cache_use_stale  error timeout updating; # Usar caché viejo si el backend falla
            proxy_cache_lock       on;         # Solo un request pasa al backend si el caché expiró

            # Header para debug: HIT, MISS, BYPASS, EXPIRED
            add_header X-Cache-Status $upstream_cache_status;
        }

        # Rutas que nunca deben cachearse
        location /api/auth {
            proxy_pass http://127.0.0.1:3000;
            proxy_no_cache 1;
            proxy_cache_bypass 1;
        }
    }
}

proxy_cache_lock es especialmente importante. Sin él, si el caché expira y llegan 500 peticiones simultáneas al mismo endpoint, las 500 pasan al backend al mismo tiempo — un fenómeno llamado cache stampede. Con proxy_cache_lock, solo la primera petición pasa al backend; las demás esperan y luego usan la respuesta cacheada.

El header X-Cache-Status con valor HIT, MISS o BYPASS es invaluable para debug y para verificar que el caché funciona correctamente.

Rate limiting

Sin rate limiting, un solo cliente puede saturar tu servidor con miles de peticiones por segundo. Nginx implementa rate limiting de forma nativa y eficiente usando el algoritmo leaky bucket.

http {
    # Definir zona: 10MB de memoria para rastrear IPs, 10 requests/segundo por IP
    limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;

    # Zona más estricta para endpoints de autenticación
    limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;

    server {
        # Aplicar rate limit general
        location / {
            limit_req zone=general burst=20 nodelay;
            proxy_pass http://127.0.0.1:3000;
        }

        # Rate limit estricto en login
        location /api/auth/login {
            limit_req zone=login burst=5;
            proxy_pass http://127.0.0.1:3000;
        }
    }
}

El parámetro burst define cuántas peticiones extra puede acumular un cliente momentáneamente antes de ser rechazado. Con rate=10r/s y burst=20, un cliente puede hacer hasta 30 peticiones instantáneas, pero después de eso Nginx limita a 10 por segundo. nodelay procesa las peticiones del burst inmediatamente sin agregar latencia artificial.

Sin nodelay, Nginx distribuye las peticiones del burst uniformemente en el tiempo — útil para proteger backends lentos, pero añade latencia visible al usuario.

Errores comunes al configurar Nginx

Reiniciar en lugar de recargar

El antipatrón más común en equipos que empiezan:

# ANTIPATRÓN: corta todas las conexiones activas
sudo systemctl restart nginx

# CORRECTO: recarga la configuración sin interrumpir conexiones activas
sudo nginx -t              # Validar configuración primero
sudo systemctl reload nginx

nginx -t prueba la configuración sin aplicarla. Un error de sintaxis con restart deja el servidor caído. Con reload, si la validación falla, Nginx rechaza la operación y sigue corriendo con la configuración anterior.

Permisos de archivos

Nginx corre como el usuario www-data (en Debian/Ubuntu) o nginx (en RHEL/CentOS). Si los archivos estáticos son propiedad de otro usuario, Nginx devuelve 403 Forbidden aunque la ruta sea correcta.

# Verificar usuario de nginx
ps aux | grep nginx

# Dar permisos correctos a los archivos
sudo chown -R www-data:www-data /var/www/mi-sitio
sudo chmod -R 755 /var/www/mi-sitio

Orden de bloques location

Nginx evalúa los bloques location con reglas de prioridad específicas. Un location de prefix más corto puede “ganar” sobre uno más específico si no se usan los modificadores correctos:

# ANTIPATRÓN: /api/v1/users podría coincidir con el location /api
location /api {
    proxy_pass http://127.0.0.1:3000;
}
location /api/v1 {
    proxy_pass http://127.0.0.1:3001;
}

# CORRECTO: = para coincidencia exacta, ~* para regex, ^~ para prefix prioritario
location ^~ /api/v1 {
    proxy_pass http://127.0.0.1:3001;
}
location /api {
    proxy_pass http://127.0.0.1:3000;
}

Las reglas de prioridad de location en orden: = (exacto) > ^~ (prefix, detiene búsqueda de regex) > ~ y ~* (regex) > prefix sin modificador (el más largo gana).

Timeouts mal configurados

Para aplicaciones que tienen operaciones largas — generación de reportes, procesamiento de archivos — los timeouts por defecto de Nginx (60 segundos) pueden cortar la conexión antes de que la respuesta llegue:

location /api/reports {
    proxy_pass http://127.0.0.1:3000;
    proxy_read_timeout  300s;  # Tiempo máximo esperando respuesta del backend
    proxy_send_timeout  300s;  # Tiempo máximo enviando la petición al backend
    proxy_connect_timeout 10s; # Tiempo máximo para establecer conexión con backend
}

Aumentar timeouts globalmente es un error. Aplícalos solo en los endpoints que los necesitan.

Configuración de producción completa

Una configuración real para una aplicación web moderna combina todos los conceptos anteriores:

# /etc/nginx/conf.d/mi-app.conf

upstream app {
    least_conn;
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
    keepalive 32;  # Mantener conexiones TCP abiertas al backend — reduce overhead
}

proxy_cache_path /var/cache/nginx/app
                 levels=1:2
                 keys_zone=app_cache:10m
                 max_size=500m
                 inactive=30m;

limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;

server {
    listen 80;
    server_name mi-app.com www.mi-app.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name mi-app.com www.mi-app.com;

    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;
    ssl_session_timeout 1d;

    server_tokens off;

    add_header Strict-Transport-Security "max-age=63072000" always;
    add_header X-Content-Type-Options    "nosniff" always;
    add_header Referrer-Policy           "strict-origin-when-cross-origin" always;

    gzip on;
    gzip_vary on;
    gzip_comp_level 6;
    gzip_types text/plain text/css application/javascript application/json image/svg+xml;

    # Assets estáticos con caché agresiva
    location ~* \.(js|css|woff2|woff)$ {
        root /var/www/mi-app/dist;
        expires 1y;
        add_header Cache-Control "public, max-age=31536000, immutable";
        gzip_static on;
        access_log off;
    }

    location ~* \.(jpg|png|webp|svg|ico)$ {
        root /var/www/mi-app/dist;
        expires 30d;
        add_header Cache-Control "public, max-age=2592000";
        access_log off;
    }

    # API con rate limiting
    location /api/ {
        limit_req zone=api burst=50 nodelay;
        proxy_pass http://app;
        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            app_cache;
        proxy_cache_valid      200 5m;
        proxy_cache_use_stale  error timeout;
        add_header X-Cache-Status $upstream_cache_status;
    }

    # Frontend SPA — todas las rutas sirven index.html
    location / {
        root /var/www/mi-app/dist;
        try_files $uri $uri/ /index.html;
        expires -1;
        add_header Cache-Control "no-cache";
    }
}

La directiva http2 en listen 443 ssl http2 activa HTTP/2, que permite multiplexar múltiples peticiones sobre una sola conexión TCP — especialmente útil para páginas con muchos assets. Los navegadores modernos solo usan HTTP/2 sobre HTTPS.

La directiva try_files $uri $uri/ /index.html es esencial para Single Page Applications. Sin ella, un usuario que navega directamente a /dashboard recibiría un 404 porque ese archivo no existe en disco — la ruta solo existe en el router del frontend.

Lo que esto significa para el negocio

Una configuración de Nginx bien hecha no es un detalle técnico. Es un multiplicador de impacto.

Un sitio sin compresión gzip en producción sirve assets 4 veces más grandes de lo necesario. En un mercado latinoamericano donde una fracción significativa de usuarios tiene conexiones móviles lentas, eso se traduce en abandono real. Google PageSpeed y Core Web Vitals penalizan los sitios lentos en rankings de búsqueda.

Una política de caché incorrecta — cachear el HTML o no cachear los assets — significa que cada visita descarga todo desde cero, y cada deploy no invalida lo que el usuario ya tiene guardado. He visto bugs que “nadie podía reproducir” que resultaban ser versiones viejas de JavaScript viviendo indefinidamente en el caché del navegador.

Un servidor sin rate limiting ni headers de seguridad es una superficie de ataque abierta. No porque los atacantes sean sofisticados, sino porque hay bots automatizados escaneando internet constantemente. La protección básica no requiere un especialista en seguridad — requiere cuatro líneas de configuración en Nginx.

El costo de configurar bien Nginx una vez es de horas. El costo de no haberlo hecho se paga durante años.

Nginx no hace tu aplicación más rápida. Hace que la velocidad que tu aplicación ya tiene llegue al usuario sin perderse en el camino.