Tema
Nginx
Nginx (se pronuncia "engine-x") es el servidor web más usado en VPS modernos. Es muy rápido sirviendo archivos estáticos, consume poca memoria y es excelente como reverse proxy: la puerta de entrada que reparte el tráfico a tus apps de Node, PHP-FPM, Python o contenedores Docker.
Concepto
- Server block: el equivalente al virtual host de Apache: un bloque
server { ... }por sitio. - location: reglas según la ruta (
location /api/ { ... }). - Reverse proxy: Nginx recibe la petición en el puerto 80/443 y la reenvía a una app interna (por ejemplo
127.0.0.1:3000). El usuario nunca habla directo con tu app. - Sin .htaccess: toda la configuración vive en archivos del servidor. Es más rápido, pero necesitas acceso al servidor para cambiarla.
txt
┌──────────────► /var/www/sitio (archivos estáticos)
Internet ──► Nginx ┼──────────────► PHP-FPM (socket) para .php
(80/443) └──────────────► Node en 127.0.0.1:3000 (proxy)Instalación (Ubuntu 24.04)
bash
sudo apt update
sudo apt install nginx -y
sudo ufw allow 'Nginx Full'
sudo systemctl status nginx
curl -I http://localhostEstructura
txt
/etc/nginx/
├── nginx.conf configuración global (workers, gzip, includes)
├── sites-available/ server blocks disponibles
├── sites-enabled/ enlaces simbólicos a los activos
├── snippets/ fragmentos reutilizables
└── conf.d/ configuraciones extra (.conf)
/var/log/nginx/ access.log y error.logComandos clave
bash
sudo nginx -t # probar sintaxis: SIEMPRE antes de recargar
sudo systemctl reload nginx # aplicar cambios
sudo ln -s /etc/nginx/sites-available/misitio /etc/nginx/sites-enabled/ # activar
sudo rm /etc/nginx/sites-enabled/default # quitar el sitio por defectoEjemplo 1: sitio estático (HTML, VitePress, Astro)
bash
sudo mkdir -p /var/www/misitio.com/public
sudo chown -R $USER:www-data /var/www/misitio.com
sudo nano /etc/nginx/sites-available/misitio.comnginx
# /etc/nginx/sites-available/misitio.com
server {
listen 80;
listen [::]:80;
server_name misitio.com www.misitio.com;
root /var/www/misitio.com/public;
index index.html;
# Logs propios
access_log /var/log/nginx/misitio.com.access.log;
error_log /var/log/nginx/misitio.com.error.log;
location / {
# 1) archivo exacto, 2) archivo.html (URLs limpias de VitePress), 3) carpeta, 4) 404
try_files $uri $uri.html $uri/ =404;
}
# Archivos con hash: caché de un año
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# Bloquear archivos ocultos (.git, .env)
location ~ /\. {
deny all;
}
error_page 404 /404.html;
}bash
sudo ln -s /etc/nginx/sites-available/misitio.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginxEjemplo 2: SPA (React, Vue, Angular)
En una SPA todas las rutas las resuelve JavaScript, así que cualquier ruta desconocida debe devolver index.html:
nginx
server {
listen 80;
server_name app.misitio.com;
root /var/www/app/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# index.html nunca en caché (para que se vean los despliegues nuevos)
location = /index.html {
add_header Cache-Control "no-cache";
}
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}Ejemplo 3: PHP con PHP-FPM
Nginx no ejecuta PHP: se lo pasa a PHP-FPM por un socket.
bash
sudo apt install php8.3-fpm -y
ls /run/php/ # verás php8.3-fpm.socknginx
server {
listen 80;
server_name tienda.com www.tienda.com;
root /var/www/tienda.com/public;
index index.php index.html;
# Front controller: todo lo que no exista va a index.php
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
location ~ /\.(?!well-known) {
deny all;
}
client_max_body_size 20M; # tamaño máximo de subida
}Más detalles en LAMP y LEMP.
Ejemplo 4: reverse proxy a una app Node
Tu app Express escucha en 127.0.0.1:3000 (ver Node con PM2):
nginx
server {
listen 80;
server_name api.misitio.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
# Pasar información real del cliente a la 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;
# Soporte para WebSockets (Socket.IO, HMR, etc.)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 60s;
}
}En Express agrega app.set('trust proxy', 1) para que req.ip y req.protocol sean correctos detrás de Nginx.
Frontend y API en el mismo dominio
nginx
server {
listen 80;
server_name misitio.com;
root /var/www/misitio/dist;
# La API va a Node
location /api/ {
proxy_pass http://127.0.0.1:3000; # sin barra final: conserva /api en la ruta
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Todo lo demás es la SPA
location / {
try_files $uri $uri/ /index.html;
}
}La barra final en proxy_pass
proxy_pass http://127.0.0.1:3000; envía /api/usuarios tal cual. proxy_pass http://127.0.0.1:3000/; (con barra) quita el prefijo /api/ y envía /usuarios. Elige según cómo estén definidas las rutas de tu app.
Redirecciones
nginx
# www → sin www
server {
listen 80;
server_name www.misitio.com;
return 301 https://misitio.com$request_uri;
}
# Página vieja → nueva (dentro de un server block)
location = /contacto.html {
return 301 /contacto;
}Ajustes globales recomendados
En /etc/nginx/nginx.conf, dentro de http { ... }:
nginx
http {
# Ocultar versión de Nginx
server_tokens off;
# Compresión gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_min_length 256;
gzip_types text/plain text/css application/json application/javascript
text/xml application/xml image/svg+xml font/ttf;
# Tamaño máximo de subida por defecto
client_max_body_size 10M;
# ... resto de la configuración original (include sites-enabled, etc.)
}Cabeceras de seguridad reutilizables en /etc/nginx/snippets/seguridad.conf:
nginx
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;Y en cada server block: include snippets/seguridad.conf;
Herencia de add_header
Si un location define su propio add_header, deja de heredar los del nivel server. Si pones cabeceras de caché en location /assets/, vuelve a incluir ahí el snippet de seguridad.
Equivalencias Apache ↔ Nginx
| Apache | Nginx |
|---|---|
<VirtualHost> | server { } |
ServerName / ServerAlias | server_name |
DocumentRoot | root |
DirectoryIndex | index |
RewriteRule + RewriteCond -f/-d | try_files |
Redirect 301 | return 301 |
ProxyPass | proxy_pass |
.htaccess | No existe: todo va en el server block |
a2ensite | ln -s a sites-enabled |
apachectl configtest | nginx -t |
Logs y depuración
bash
sudo tail -f /var/log/nginx/misitio.com.error.log
sudo nginx -T | less # ver la configuración completa cargada
curl -I http://misitio.com # ver cabeceras de respuesta| Error | Causa típica |
|---|---|
| 502 Bad Gateway | La app (Node/PHP-FPM) está caída o el puerto/socket es incorrecto. |
| 504 Gateway Timeout | La app tarda demasiado: revisa la app o sube proxy_read_timeout. |
| 413 Request Entity Too Large | Sube client_max_body_size. |
| 403 Forbidden | Permisos de carpeta o falta index. |
| 404 en rutas de la SPA | Falta try_files ... /index.html. |
Buena práctica
Un archivo por sitio en sites-available, nombrado con el dominio. Así, con un vistazo a la carpeta sabes qué hospeda el servidor.
Ejercicios
- Escribe un server block para
docs.cliente.comque sirva un VitePress compilado en/var/www/docs/distcon URLs limpias. - Configura
panel.cliente.comcomo reverse proxy a una app en el puerto 4000 con soporte de WebSockets. - Tras un cambio, Nginx no recarga. ¿Qué comando te dice el error exacto?
Ver solución
nginx
server {
listen 80;
server_name docs.cliente.com;
root /var/www/docs/dist;
index index.html;
location / {
try_files $uri $uri.html $uri/ =404;
}
error_page 404 /404.html;
}nginx
server {
listen 80;
server_name panel.cliente.com;
location / {
proxy_pass http://127.0.0.1:4000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}sudo nginx -t(ysudo journalctl -u nginx -n 30).
Resumen
- Nginx sirve estáticos muy rápido y funciona como reverse proxy.
- Un
serverpor sitio ensites-available, activado con un enlace ensites-enabled. try_filesresuelve URLs limpias, SPAs y front controllers.- PHP se ejecuta vía PHP-FPM; Node vía
proxy_pass. nginx -tantes dereload, siempre.- Siguiente: Stack LAMP y LEMP.