Skip to content

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://localhost

Estructura ​

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.log

Comandos 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 defecto

Ejemplo 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.com
nginx
# /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 nginx

Ejemplo 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.sock
nginx
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 ​

ApacheNginx
<VirtualHost>server { }
ServerName / ServerAliasserver_name
DocumentRootroot
DirectoryIndexindex
RewriteRule + RewriteCond -f/-dtry_files
Redirect 301return 301
ProxyPassproxy_pass
.htaccessNo existe: todo va en el server block
a2ensiteln -s a sites-enabled
apachectl configtestnginx -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
ErrorCausa típica
502 Bad GatewayLa app (Node/PHP-FPM) está caída o el puerto/socket es incorrecto.
504 Gateway TimeoutLa app tarda demasiado: revisa la app o sube proxy_read_timeout.
413 Request Entity Too LargeSube client_max_body_size.
403 ForbiddenPermisos de carpeta o falta index.
404 en rutas de la SPAFalta 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 ​

  1. Escribe un server block para docs.cliente.com que sirva un VitePress compilado en /var/www/docs/dist con URLs limpias.
  2. Configura panel.cliente.com como reverse proxy a una app en el puerto 4000 con soporte de WebSockets.
  3. 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";
    }
}
  1. sudo nginx -t (y sudo journalctl -u nginx -n 30).

Resumen ​

  • Nginx sirve estáticos muy rápido y funciona como reverse proxy.
  • Un server por sitio en sites-available, activado con un enlace en sites-enabled.
  • try_files resuelve URLs limpias, SPAs y front controllers.
  • PHP se ejecuta vía PHP-FPM; Node vía proxy_pass.
  • nginx -t antes de reload, siempre.
  • Siguiente: Stack LAMP y LEMP.

Academia Web — aprende, copia y construye.