nginxservidor webdevopsproxy inverso

Configuración de Nginx explicada: estático, SPA y proxy inverso

Los tres bloques server que necesitas: try_files para SPAs, proxy inverso con cabeceras X-Forwarded, HTTPS con Let's Encrypt y la trampa de herencia de add_header.

23 de agosto de 2026·8 min de lectura

Casi toda configuración de Nginx en la web real se reduce a tres patrones: servir archivos estáticos, servir una SPA con rutas de cliente, o hacer de proxy inverso hacia una aplicación. Si entiendes bien esos tres bloques y dos o tres directivas clave, puedes mantener cualquier servidor. Este artículo los desmonta con la configuración exacta.

Patrón 1: sitio estático

server {
    listen 80;
    http2 off;
    server_name midominio.com;

    root /var/www/midominio;
    index index.html index.htm;
    client_max_body_size 10M;

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

La directiva que hace todo el trabajo es try_files: prueba las opciones en orden y entrega la primera que exista. Aquí: ¿existe un archivo que coincida exactamente con la URI? ¿Un directorio? Si nada existe, devuelve 404. Sin try_files, Nginx usaría su comportamiento por defecto, mucho menos predecible con URLs limpias.

Patrón 2: SPA (React, Vue, Svelte)

Una SPA de routing cliente recibe peticiones como /usuarios/42/editar que no corresponden a ningún archivo real — el router de JavaScript debe encargarse. Cambia una sola cosa respecto al patrón anterior:

location / {
    try_files $uri $uri/ /index.html;
}

El fallback final ya no es =404 sino /index.html: cualquier ruta desconocida sirve la aplicación, que arranca, lee la URL del navegador y renderiza la vista correspondiente. Es literalmente el cambio de una palabra el que separa un servidor que funciona con React Router de uno que devuelve 404 al refrescar cualquier página interior.

Patrón 3: proxy inverso

Aquí Nginx deja de servir archivos y pasa a reenviar cada petición a tu aplicación (Node, Python, Go...) escuchando en otro puerto:

server {
    listen 80;
    http2 off;
    server_name midominio.com;
    client_max_body_size 10M;

    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;

    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_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 60s;
    }
}

Las cabeceras proxy_set_header no son decorativas:

  • X-Forwarded-For acumula la cadena de IPs de clientes reales. Sin ella, tu aplicación ve todas las peticiones como provenientes de 127.0.0.1 — fatal para rate limiting, logs y geolocalización.
  • X-Forwarded-Proto informa de que el cliente vino por HTTPS aunque la conexión interna sea HTTP. Sin ella, frameworks que generan URLs absolutas producen http:// en producción.
  • Upgrade + Connection "upgrade" habilitan WebSockets, que necesitan un salto de protocolo que el proxy debe permitir explícitamente.
  • client_max_body_size limita el tamaño del cuerpo aceptado; por defecto Nginx corta en 1 MB y los uploads fallan con error 413 misterioso.

La trampa de herencia de add_header

Este comportamiento sorprende incluso a administradores veteranos: las cabeceras add_header NO se heredan si un bloque location define alguna propia. En este ejemplo:

server {
    add_header X-Frame-Options "DENY" always;

    location / {
        # hereda X-Frame-Options
    }

    location ~* \.(css|js|png)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
        # ¡pierde X-Frame-Options!
    }
}

El bloque de assets define su propio add_header (el de caché), así que Nginx descarta las heredadas del nivel server. Para assets estáticos es irrelevante — nadie embebe un CSS en un iframe — pero es imprescindible conocerlo cuando añades cabeceras dentro de locations con contenido dinámico.

HTTPS: redirección y certificados

Con certificados de Let's Encrypt (vía Certbot), el patrón son dos bloques server: el viejo puerto 80 solo redirige, y el 443 hace el trabajo real:

server {
    listen 80;
    server_name midominio.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    http2 on;
    server_name midominio.com;

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

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;

    # ... resto igual ...
}

Dos detalles modernos: http2 on es la sintaxis actual (desde Nginx 1.25.1); la antigua era listen 443 ssl http2;. Y HSTS con max-age=63072000 le dice a los navegadores que durante dos años ni intenten entrar por HTTP — actívalo solo cuando estés seguro de que HTTPS es estable.

Gzip pertenece al bloque http

La compresión gzip se configura en el bloque http {} del archivo principal /etc/nginx/nginx.conf, no en tu server block:

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

gzip_comp_level 6 es el punto dulce: niveles superiores ganan unos pocos bytes a cambio de bastante CPU.

Genera los tres patrones configurados

Elegir modo (estático, SPA o proxy), activar SSL, caché de assets, cabeceras de seguridad y gzip mediante toggles es lo que hace nuestro generador de configuración Nginx: rellena dominio, puertos y rutas, y copia la configuración completa lista para /etc/nginx/sites-available/.

Preguntas frecuentes

¿Dónde va mi archivo de configuración? En Debian/Ubuntu, /etc/nginx/sites-available/midominio con un symlink desde sites-enabled. Tras cambiar algo: nginx -t para validar y systemctl reload nginx para aplicar sin corte.

¿Nginx o Caddy? Caddy automatiza certificados TLS sin tocar nada y es excelente para proyectos personales. Nginx sigue siendo el estándar en producción por rendimiento crudo, ecosistema y documentación ante problemas raros.

¿Por qué mis WebSockets se caen a los 60 segundos? El proxy_read_timeout por defecto cierra conexiones sin tráfico. Con WebSockets inactivos, súbelo o implementa ping/pong a nivel de aplicación.


Genera tu configuración completa con el generador de Nginx, con SSL y cabeceras de seguridad incluidas.

Pruébalo sin código

Configuración Nginx

Estático, SPA o proxy inverso.

Abrir Configuración Nginx

Hecho por

Miguel Ángel Colorado Marin (MACM)

Full-Stack Developer · Guadalajara, España

Desarrollo aplicaciones web, herramientas digitales y proyectos completos — desde el diseño hasta el despliegue.

Contáctame