Lección 1 de 212 min
Nginx como servidor web
Su arquitectura basada en eventos, la configuración con server y location, archivos estáticos, compresión y TLS.
Has terminado tu web y ejecutas npm run build. Tienes una carpeta llena de archivos HTML, CSS, JavaScript e imágenes. ¿Quién los entrega cuando alguien escribe tu dirección en el navegador? ¿Quién se encarga del candado de HTTPS, de comprimir los archivos o de decirle al navegador cuánto tiempo puede guardarlos?
Esa pieza es el servidor web, y Nginx es uno de los más usados del mundo. En esta lección verás cómo está construido por dentro, cómo se configura y cómo servir una web estática rápida y segura. En la siguiente lo usarás como proxy inverso y balanceador.
Qué es y cómo funciona por dentro
Qué hace un servidor web
El programa que escucha peticiones HTTP y responde con archivos o con lo que diga tu app.
- Ejemplos:
- Nginx, Apache, Caddy
- Puertos:
- 80 y 443
Un servidor web es un programa que se queda escuchando en un puerto (el 80 para HTTP, el 443 para HTTPS). Cuando llega una petición, la lee (método, ruta, cabeceras), decide qué hacer con ella y devuelve una respuesta con su código de estado, sus cabeceras y su contenido.
Para un archivo estático, el trabajo es buscarlo en el disco y enviarlo con el tipo correcto (Content-Type). Para lo dinámico, se lo pasa a tu aplicación (Node.js, Python, PHP…) y reenvía su respuesta. Por el camino suele ocuparse de HTTPS, la compresión, la caché y los registros de acceso.
Úsalo cuando
Siempre hay uno: tu web, una API o un panel interno necesitan algo que reciba las peticiones. La pregunta es si usas uno dedicado como Nginx, Apache o Caddy, o el que trae tu plataforma de hosting.
Ten en cuenta
En plataformas como Vercel o Netlify no lo configuras tú, pero sigue existiendo. Y el servidor que trae tu framework para desarrollar (next dev, vite) no está pensado para recibir tráfico real.
GET /img/logo.png HTTP/1.1
Host: mitienda.com
Accept-Encoding: gzip, br
HTTP/1.1 200 OK
Content-Type: image/png
Content-Length: 4821
Cache-Control: max-age=604800
Server: nginxLa arquitectura de Nginx: maestro y workers
Pocos procesos que atienden miles de conexiones a la vez gracias a un bucle de eventos.
- Creado por:
- Igor Sysoev
- Desde:
- 2004
- Modelo:
- Basado en eventos
Al arrancar, Nginx crea un proceso maestro que lee la configuración, abre los puertos y lanza varios procesos worker, normalmente uno por núcleo de CPU. El maestro no atiende peticiones: solo vigila a los workers y los sustituye si hace falta.
Cada worker atiende miles de conexiones con un bucle de eventos y E/S no bloqueante: en lugar de quedarse esperando a que un cliente lento termine de enviar datos, el sistema operativo (con epoll en Linux o kqueue en BSD y macOS) le avisa cuando una conexión tiene algo listo, y mientras tanto atiende otras. El modelo clásico de Apache, en cambio, dedicaba un proceso o un hilo a cada conexión, que consume mucha más memoria con miles de clientes a la vez.
Úsalo cuando
Es la razón por la que Nginx aguanta muchas conexiones simultáneas con poca memoria, ideal delante de apps que no deberían gestionar miles de clientes lentos.
Ten en cuenta
Como un worker atiende muchas conexiones, cualquier operación que lo bloquee (un módulo mal escrito, un disco muy lento) frena a todas. Prueba la configuración con nginx -t antes de aplicarla y recárgala con nginx -s reload: el maestro arranca workers nuevos y deja que los viejos terminen sus peticiones, sin cortar a nadie.
user www-data;
worker_processes auto; # un worker por núcleo de CPU
events {
worker_connections 1024; # conexiones simultáneas por worker
}
http {
include mime.types; # extensión → Content-Type
include /etc/nginx/conf.d/*.conf;
}
# nginx -t → comprueba la configuración
# nginx -s reload → la aplica sin cortar conexionesConfigurar Nginx
La configuración de Nginx son directivas que terminan en punto y coma, agrupadas en bloques con llaves. Los bloques se anidan: http contiene uno o varios server (un sitio web cada uno), y cada server contiene bloques location para las distintas rutas. Una directiva escrita en un bloque exterior la heredan los interiores.
El bloque server
Define un sitio: en qué puerto escucha y a qué dominios responde.
- Elige por:
- Cabecera Host
- Directivas:
- listen, server_name
Un bloque server es un servidor virtual: con listen dices el puerto y con server_name, los dominios. Así, una sola máquina puede servir muchas webs distintas. Nginx elige el bloque comparando la cabecera Host de la petición con los server_name de los bloques que escuchan en ese puerto.
Si ninguno coincide, se usa el servidor por defecto de ese puerto: el primero que aparece o el que marques con listen 80 default_server.
Úsalo cuando
Para cada dominio o subdominio que sirvas, y para separar la versión HTTP (que solo redirige) de la HTTPS.
Ten en cuenta
Si alguien entra por la IP o con un dominio que no esperas, responderá el servidor por defecto, que quizá sea otra de tus webs. Muchos equipos definen uno por defecto que simplemente cierra la conexión con return 444;.
server {
listen 80;
server_name mitienda.com www.mitienda.com;
root /var/www/tienda;
}
server {
listen 80;
server_name blog.mitienda.com;
root /var/www/blog;
}Los bloques location
Reglas distintas según la ruta: exactas, por prefijo o con expresiones regulares.
- Modificadores:
- =, ^~, ~, ~*
- Gana antes:
- La coincidencia exacta
Dentro de un server, cada location aplica una configuración a las rutas que encajan con él. Hay cuatro tipos: = para una ruta exacta, un prefijo normal como /api/, ^~ para un prefijo que gana a las expresiones regulares, y ~ o ~* para expresiones regulares (la segunda sin distinguir mayúsculas).
El orden de elección no es el del archivo: primero se busca una coincidencia exacta; si no la hay, Nginx recuerda el prefijo más largo que encaja. Si ese prefijo lleva ^~, lo usa; si no, prueba las expresiones regulares en orden y se queda con la primera que encaje. Solo si ninguna lo hace usa el prefijo recordado.
Úsalo cuando
Para tratar de forma distinta las imágenes, la API, el panel de administración o una ruta de salud que consulte tu monitorización.
Ten en cuenta
La trampa típica: una expresión regular como ~* \.png$ gana a un prefijo normal más largo, como /imagenes/. Si quieres que el prefijo mande, márcalo con ^~. En la tabla comparativa de esta lección tienes las cuatro variantes.
server {
listen 80;
server_name mitienda.com;
root /var/www/tienda;
location = /salud { # solo /salud, exacta
return 200 "ok";
}
location ^~ /imagenes/ { # prefijo que gana a las regex
expires 30d;
}
location ~* \.(png|jpe?g|webp)$ { # regex, sin distinguir mayúsculas
expires 7d;
}
location / { # todo lo demás
try_files $uri $uri/ =404;
}
}root y try_files
Dónde están los archivos en el disco y en qué orden buscarlos.
- Variable clave:
- $uri
- Plan B:
- =404 o una ruta
root indica la carpeta base: con root /var/www/tienda, una petición a /css/app.css se busca en /var/www/tienda/css/app.css. Es decir, Nginx añade la ruta de la petición a la carpeta.
try_files prueba varias opciones en orden y usa la primera que exista. La variable $uri es la ruta pedida; $uri/ prueba si es una carpeta con un index.html dentro. El último parámetro es el plan B: un código de error como =404 o una ruta a la que redirigir internamente.
Úsalo cuando
En todo sitio que sirva archivos desde el disco. try_files es la forma recomendada de decidir qué hacer cuando un archivo no existe.
Ten en cuenta
No confundas root con alias: alias sustituye el prefijo del location en lugar de añadirlo. Si las rutas no cuadran, revisa el registro de errores (error.log): ahí verás la ruta exacta que Nginx intentó abrir.
location / {
root /var/www/tienda;
index index.html;
# 1.º el archivo exacto, 2.º una carpeta con index.html, 3.º error 404
try_files $uri $uri/ =404;
}
location /descargas/ {
# alias sustituye el prefijo: /descargas/guia.pdf → /srv/pdf/guia.pdf
alias /srv/pdf/;
}Fallback para una SPA
Cualquier ruta desconocida devuelve index.html y el router de JavaScript hace el resto.
- Para:
- React, Vue, Angular…
- Respuesta:
- index.html con 200
En una SPA hecha con React, Vue o Angular, la ruta /productos/42 no existe como archivo: la crea el router de JavaScript en el navegador. Si el usuario recarga en esa página o la abre desde un enlace, el navegador pide /productos/42 al servidor y, sin más configuración, recibe un 404.
La solución es que, cuando no exista ningún archivo, Nginx devuelva index.html con un 200. El navegador carga la app, el router lee la URL y pinta la vista de ese producto. Es una sola línea: try_files $uri $uri/ /index.html;.
Úsalo cuando
Siempre que despliegues una SPA cuyo router use rutas normales (con la History API) en lugar de rutas con #.
Ten en cuenta
Con el fallback, una ruta que no existe ya no da un 404 de verdad, sino la app. Excluye lo que no es parte de la app: las rutas de la API (/api/) y los recursos estáticos, para que un .js que falta dé 404 en lugar de devolver HTML. Y muestra en la app una página de «no encontrado» para las rutas que el router no conozca.
server {
listen 80;
server_name app.mitienda.com;
root /var/www/app/dist;
# Los recursos que faltan dan un 404 de verdad
location /assets/ {
try_files $uri =404;
}
# Cualquier otra ruta (/perfil, /productos/42) carga la SPA
location / {
try_files $uri $uri/ /index.html;
}
}Servir archivos estáticos rápido
Una vez que Nginx encuentra el archivo, quedan dos preguntas para que la web vuele: ¿cuánto ocupa por el camino? y ¿hace falta descargarlo otra vez en la siguiente visita?
Compresión con gzip
Reduce mucho lo que ocupan HTML, CSS y JavaScript antes de enviarlos.
- Cabecera:
- Content-Encoding
- Comprime:
- Texto, no imágenes
El navegador anuncia qué compresiones entiende con la cabecera Accept-Encoding. Con gzip on, Nginx comprime la respuesta al vuelo y responde con Content-Encoding: gzip. Los formatos de texto como HTML, CSS, JavaScript, JSON o SVG suelen quedarse en una fracción de su tamaño original.
Para no comprimir en cada petición, tu build puede generar versiones ya comprimidas (app.js.gz) y gzip_static on las sirve directamente. Brotli comprime algo más que gzip, pero en el Nginx de código abierto requiere un módulo adicional.
Úsalo cuando
Siempre para los formatos de texto. Es de las mejoras de rendimiento más baratas que existen.
Ten en cuenta
Por defecto, Nginx solo comprime text/html: añade los demás tipos con gzip_types. No comprimas imágenes JPEG, PNG o WebP, ni vídeos o fuentes woff2: ya están comprimidos y solo gastarías CPU. Y activa gzip_vary para que las cachés intermedias guarden por separado la versión comprimida y la normal.
http {
gzip on;
gzip_comp_level 5;
gzip_min_length 1024; # con archivos diminutos no compensa
gzip_vary on; # añade Vary: Accept-Encoding
gzip_types text/css application/javascript application/json image/svg+xml;
# text/html se comprime siempre que gzip esté activo
# Sirve app.js.gz si existe (el módulo viene en la mayoría de paquetes)
gzip_static on;
}Cabeceras de caché
Le dicen al navegador cuánto tiempo puede reutilizar un archivo sin volver a pedirlo.
- Cabecera:
- Cache-Control
- Revalidar:
- ETag y 304
Con Cache-Control: max-age=31536000 (un año en segundos), el navegador reutiliza el archivo sin preguntar al servidor. En Nginx lo pones con expires 1y o con add_header Cache-Control …. Además, Nginx acompaña cada archivo estático de un ETag y un Last-Modified: cuando la copia caduca, el navegador pregunta «¿ha cambiado?» y, si no, recibe un 304 Not Modified sin contenido.
La estrategia habitual combina dos reglas. Los archivos con un hash en el nombre (app.3f9a1c.js), que generan los bundlers, nunca cambian: si cambia el contenido, cambia el nombre. Se cachean un año con immutable. El index.html, que es el que apunta a esos nombres, se sirve con no-cache para que el navegador compruebe siempre si hay una versión nueva.
Úsalo cuando
En cualquier web con recursos estáticos. Bien configurada, la segunda visita apenas descarga nada.
Ten en cuenta
Si cacheas mucho tiempo un archivo sin hash en el nombre, los usuarios seguirán viendo la versión vieja después de publicar. Y cuidado con una regla de Nginx: si un location tiene sus propios add_header, no hereda los del bloque superior, que desaparecen sin avisar.
# Con hash en el nombre (app.3f9a1c.js): no cambian nunca
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
# index.html: el navegador debe comprobar siempre si hay versión nueva
location = /index.html {
add_header Cache-Control "no-cache";
}
# Imágenes sin hash: una semana
location /imagenes/ {
expires 7d;
}Pruébalo
Abre las herramientas de desarrollo en la pestaña Red y recarga esta página. Haz clic en un archivo .js y busca content-encoding y cache-control en las cabeceras de la respuesta. Después recarga otra vez: los recursos cacheados aparecerán como servidos desde la caché.
HTTPS
Hoy toda web debería ir por HTTPS: los navegadores marcan como «no seguras» las que no lo hacen y muchas APIs solo funcionan con él. Nginx es un sitio cómodo para resolverlo una sola vez, delante de todo lo demás.
Terminación TLS
Nginx descifra el HTTPS y habla HTTP normal con tu aplicación en la red interna.
- Puerto:
- 443
- Certificados:
- Let's Encrypt
TLS es el protocolo que cifra la conexión en HTTPS. «Terminar» TLS significa que Nginx tiene el certificado y la clave privada, negocia el cifrado con el navegador y descifra las peticiones. Después las pasa a tu app por HTTP normal, dentro de la máquina o de la red privada.
Así, tu aplicación no tiene que saber nada de certificados, y los renuevas en un solo sitio. Let's Encrypt emite certificados gratuitos y herramientas como Certbot los renuevan automáticamente.
Úsalo cuando
Delante de cualquier aplicación que exponga una web o una API a internet. También es donde se activa HTTP/2.
Ten en cuenta
El tramo entre Nginx y tu app va sin cifrar: está bien dentro de la misma máquina o de una red privada de confianza, pero no si atraviesa redes ajenas. Desactiva las versiones antiguas de TLS y vigila la caducidad de los certificados. En versiones antiguas de Nginx, HTTP/2 se activaba con listen 443 ssl http2; en las recientes se usa http2 on.
server {
listen 443 ssl;
http2 on;
server_name mitienda.com;
ssl_certificate /etc/letsencrypt/live/mitienda.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mitienda.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
# Nginx descifra y habla HTTP normal con la app, en la misma máquina
proxy_pass http://127.0.0.1:3000;
}
}Redirección de HTTP a HTTPS
Quien entra por http:// acaba siempre en https://, con la misma ruta.
- Código:
- 301
- Cabecera:
- Strict-Transport-Security
Aunque tu web vaya por HTTPS, mucha gente escribe la dirección sin protocolo y el navegador puede probar primero con http://. Un bloque server en el puerto 80 que solo hace return 301 https://$host$request_uri; los manda a la versión segura conservando el dominio y la ruta.
Para que el navegador ni siquiera intente la versión insegura en las siguientes visitas, se añade la cabecera HSTS (Strict-Transport-Security): le dice que, durante un tiempo, use siempre HTTPS para ese dominio.
Úsalo cuando
En todo sitio con HTTPS. La redirección y HSTS son el complemento obligado de la terminación TLS.
Ten en cuenta
HSTS es difícil de deshacer: si lo activas con un max-age largo y luego HTTPS falla, los usuarios no podrán entrar. Empieza con un valor corto y súbelo cuando todo esté estable. Y los navegadores recuerdan las redirecciones 301: mientras pruebas, usa 302.
server {
listen 80;
server_name mitienda.com www.mitienda.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name mitienda.com www.mitienda.com;
# …certificados como en el ejemplo anterior
# HSTS: «durante un año, ven siempre por HTTPS»
add_header Strict-Transport-Security "max-age=31536000" always;
}Interactivo
Los cuatro tipos de location
Ejemplos con la petición /imagenes/logo.png.
| Característica | = (exacta) | ^~ (prefijo preferente) | ~ y ~* (regex) |
|---|---|---|---|
| Cómo compara | La ruta completa, carácter a carácter | El comienzo de la ruta | Una expresión regular; ~* sin distinguir mayúsculas |
| Prioridad | La más alta: si coincide, se usa sin buscar más | Si es el prefijo más largo, gana a las regex | La primera que encaje, en orden de aparición |
| Ejemplo | location = /salud | location ^~ /imagenes/ | location ~* \.png$ |
| ¿Encaja con /imagenes/logo.png? | No: solo encajaría /salud | Sí, y ninguna regex se lo quita | Sí, y gana a un prefijo normal |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
Tienes una sola máquina y quieres servir en ella la tienda (mitienda.com) y el blog (blog.mitienda.com), cada uno desde su carpeta.
Para recordar
- Un servidor web escucha peticiones HTTP y responde con archivos o pasándolas a tu app; Nginx lo hace con pocos procesos y un bucle de eventos.
serverelige el sitio por la cabeceraHost;locationelige la regla por la ruta, y las expresiones regulares ganan a los prefijos normales salvo con^~.- Una SPA necesita
try_files $uri $uri/ /index.html, excluyendo la API y los recursos estáticos. - Comprime los formatos de texto, cachea un año los archivos con hash y sirve
index.htmlconno-cache. - Termina TLS en Nginx, redirige HTTP a HTTPS y añade HSTS empezando con un valor corto.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.Con
try_files $uri $uri/ /index.html;, alguien pide/productos/42y no existe ese archivo ni esa carpeta. ¿Qué responde Nginx?2.¿Qué ocurre al ejecutar
nginx -s reloadcon una configuración nueva y correcta?3.Hay tres reglas:
location /,location /imagenes/ylocation ~* \.png$. ¿Cuál atiende/imagenes/logo.png?4.Tu bundler genera
app.3f9a1c.jsy elindex.htmlque lo carga. ¿Qué caché pones a cada uno?5.¿Qué significa que Nginx haga «terminación TLS» delante de tu aplicación?