En un VPS pequeño de 512 MB a 2 GB de RAM, Nginx gana bastante velocidad con unos pocos cambios: workers bien dimensionados, keepalive con clientes y backends, compresión, reutilización de sesiones TLS, HTTP/2 o HTTP/3, cabeceras de caché para el navegador y una caché FastCGI o de proxy de corta duración. Esta guía repasa cada punto con configuraciones para Debian 12/13 y Ubuntu 22.04/24.04 (con notas para Rocky Linux/AlmaLinux) y te enseña a medir el resultado.
sudo cp -a /etc/nginx /root/nginx-backup-$(date +%F).Paso 1: Mide una línea base
Mide primero para saber qué ha ayudado de verdad. Para una sola petición, curl muestra en qué se va el tiempo:
curl -so /dev/null -w 'dns %{time_namelookup}s connect %{time_connect}s tls %{time_appconnect}s ttfb %{time_starttransfer}s total %{time_total}s http/%{http_version}\n' https://example.com/
connect menos dns es aproximadamente el RTT de red, tls es el handshake y ttfb (tiempo hasta el primer byte) menos tls es lo que tardó el servidor en generar la página. Un ttfb alto en una página PHP apunta a la aplicación, y ahí es donde más ayuda la caché (paso 7).
Para medir el rendimiento bajo carga, usa wrk, ab o hey, mejor desde otra máquina para que la herramienta no le quite CPU a Nginx:
# Debian/Ubuntu: sudo apt install wrk apache2-utils
# Rocky/Alma: sudo dnf install httpd-tools
wrk -t2 -c50 -d30s https://example.com/
ab -n 2000 -c 20 -k https://example.com/
hey -n 2000 -c 50 https://example.com/
Apunta las peticiones por segundo y la latencia, y repite el mismo comando después de cada cambio.
Paso 2: Comprueba la versión de Nginx y sus módulos
nginx -v
nginx -V 2>&1 | tr ' ' '\n' | grep -E 'http_v2|http_v3'
ls /etc/nginx/modules-enabled/ 2>/dev/null
nproc; free -m
grep -nE 'worker_|gzip|keepalive_timeout|access_log|ssl_session' /etc/nginx/nginx.conf
La versión determina qué sintaxis puedes usar. Debian 12 trae la 1.22, Debian 13 la 1.26, Ubuntu 22.04 la 1.18 y Ubuntu 24.04 la 1.24. HTTP/3 necesita Nginx 1.25 o posterior compilado con --with-http_v3_module; si el paquete de tu distribución no lo incluye, el repositorio oficial de nginx.org ofrece versiones actuales que sí lo traen. El último comando muestra ajustes que ya existen en nginx.conf: antes del paso 4, comenta ahí gzip on;, keepalive_timeout, access_log y cualquier línea ssl_session_*; si no, nginx -t avisará de una directiva duplicada o cada petición se registrará dos veces.
Paso 3: Dimensiona los workers
# /etc/nginx/nginx.conf - main context (outside http {})
worker_processes auto;
worker_rlimit_nofile 8192;
events {
worker_connections 2048;
}
worker_processes auto arranca un worker por núcleo de CPU. Cada worker puede mantener hasta worker_connections conexiones a la vez, y una petición con proxy usa dos (cliente y backend). worker_rlimit_nofile sube el límite de archivos abiertos de los workers; mantenlo al menos al doble de worker_connections. Valores sugeridos:
| Plan | worker_connections | worker_rlimit_nofile |
|---|---|---|
| mini (1 núcleo, 512 MB) | 1024 | 4096 |
| 01 (2 núcleos, 1 GB) | 2048 | 8192 |
| 02 (3 núcleos, 2 GB) | 4096 | 16384 |
Nginx usa muy poca memoria por conexión. En planes pequeños, el límite real suele ser PHP-FPM o tu aplicación, así que ajusta esos pools a la RAM que tienes.
Paso 4: Keepalive, compresión, sesiones TLS, caché de archivos y logs
Pon los ajustes comunes del nivel http en un solo archivo. Tanto Debian/Ubuntu como Rocky/Alma cargan /etc/nginx/conf.d/*.conf dentro de http {}:
sudo tee /etc/nginx/conf.d/00-performance.conf > /dev/null <<'EOF'
# Loaded inside http {} by nginx.conf
# Keepalive with clients
keepalive_timeout 30s;
keepalive_requests 1000;
# Compression
gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_vary on;
gzip_proxied any;
gzip_types text/plain text/css text/xml text/javascript application/javascript
application/json application/xml application/rss+xml image/svg+xml;
# TLS session reuse
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# Cache open file descriptors and metadata
open_file_cache max=2000 inactive=60s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
# Buffered access log
access_log /var/log/nginx/access.log combined buffer=64k flush=5s;
EOF
- Keepalive: el navegador reutiliza una conexión para muchas peticiones y se ahorra un handshake TCP y TLS cada vez. 30 segundos es un buen equilibrio en un servidor pequeño; los tiempos muy largos solo mantienen abiertas conexiones inactivas.
- Gzip: el nivel 5 consigue casi toda la reducción de tamaño con poco uso de CPU.
text/htmlse comprime siempre, por eso no está en la lista. No añadas imágenes JPEG o PNG ni archivos comprimidos: ya lo están. - Caché de sesiones TLS: los visitantes que vuelven reanudan su sesión TLS sin un handshake completo. 10 MB guardan unas 40.000 sesiones.
- open_file_cache: guarda los descriptores y metadatos de los archivos que más se sirven. Tras un despliegue, los archivos modificados pueden tardar hasta
open_file_cache_valid(60 s) en detectarse. - Log de acceso con búfer: Nginx escribe el log en bloques de 64 KB o cada 5 segundos en lugar de en cada petición, lo que reduce las escrituras en disco. Las líneas aparecen con un pequeño retraso.
Brotli (opcional)
Brotli comprime los recursos de texto un 15–20% mejor que gzip, pero no forma parte de Nginx estándar. Algunas versiones de Debian y Ubuntu lo empaquetan como módulo dinámico; compruébalo primero:
apt-cache search libnginx-mod-http-brotli
sudo apt install libnginx-mod-http-brotli-filter libnginx-mod-http-brotli-static
Si la búsqueda no devuelve nada, tu versión no lo empaqueta, y los repositorios base de Rocky/Alma tampoco lo incluyen. En ese caso, quédate con gzip, que funciona perfectamente. Si el módulo se instala, añade:
# Add to /etc/nginx/conf.d/00-performance.conf only if the module is installed
brotli on;
brotli_comp_level 5;
brotli_types text/plain text/css text/xml text/javascript application/javascript
application/json application/xml application/rss+xml image/svg+xml;
Paso 5: Keepalive hacia el backend (proxy inverso)
Si Nginx hace de proxy hacia una aplicación (Node.js, Python, Go, etc.), mantén un pool de conexiones inactivas con ella en lugar de abrir una nueva en cada petición:
upstream app_backend {
server 127.0.0.1:3000;
keepalive 16;
}
server {
# ... listen, server_name, certificates ...
location / {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
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;
}
}
keepalive 16 es el número de conexiones inactivas que guarda cada worker. El keepalive hacia el backend solo funciona con proxy_http_version 1.1 y la cabecera Connection vacía. Si la aplicación usa WebSockets, dale a ese location sus propias cabeceras Upgrade/Connection.
Paso 6: Activa HTTP/2 y HTTP/3
HTTP/2 envía muchas peticiones por una sola conexión. HTTP/3 funciona sobre QUIC (UDP) y gestiona mejor la pérdida de paquetes y los cambios de red, algo que agradecen los usuarios móviles y los visitantes lejanos. Para Nginx 1.25.1 o posterior:
server {
listen 443 ssl;
listen 443 quic reuseport;
http2 on;
http3 on;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
add_header Alt-Svc 'h3=":443"; ma=86400' always;
root /var/www/example.com;
}
reuseportsolo puede aparecer una vez por dirección y puerto en todos los bloques server. Los demás sitios en la misma IP usan sololisten 443 quic;.- QUIC siempre usa TLS 1.3, así que mantén
TLSv1.3activado. - La cabecera
Alt-Svcindica al navegador que hay HTTP/3: la primera visita va por HTTP/2 y las siguientes peticiones pasan a HTTP/3. - Si usas IPv6, añade las mismas líneas con
[::]:443.
En versiones antiguas de Nginx (paquetes de Ubuntu 22.04/24.04 y Debian 12), http2 on; no existe y HTTP/3 no está disponible. Activa HTTP/2 en la línea listen:
listen 443 ssl http2;
HTTP/3 también necesita el puerto UDP 443 abierto en el firewall (consulta cómo configurar el firewall):
# Ubuntu/Debian with ufw
sudo ufw allow 443/udp
# Rocky/Alma with firewalld
sudo firewall-cmd --permanent --add-port=443/udp
sudo firewall-cmd --reload
Comprueba la cabecera y, si tu curl está compilado con soporte HTTP/3 (curl -V muestra HTTP3), prueba la conexión directamente. Si no, mira la columna Protocolo en las herramientas de desarrollo del navegador, que debe mostrar h3:
curl -sI https://example.com/ | grep -i alt-svc
curl -sI --http3-only https://example.com/ | head -n 1
Paso 7: Cachea archivos estáticos y páginas dinámicas
Cabeceras de caché del navegador para estáticos
# Static files: let browsers cache them
location ~* \.(?:css|js|mjs|woff2|svg|png|jpe?g|gif|webp|avif|ico)$ {
expires 30d;
access_log off;
try_files $uri =404;
}
# Versioned assets (file name contains a hash, e.g. app.3f9a1c.js)
location ^~ /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Alt-Svc 'h3=":443"; ma=86400' always;
access_log off;
}
Usa la regla de un año con immutable solo para archivos cuyo nombre cambia cuando cambia su contenido; si no, los visitantes se quedarán con versiones antiguas. Ojo con una regla de Nginx: si un location tiene cualquier add_header, deja de heredar los add_header del bloque server. Por eso Alt-Svc se repite ahí; haz lo mismo con HSTS u otras cabeceras de seguridad.
Crea los directorios de caché
sudo mkdir -p /var/cache/nginx/fastcgi /var/cache/nginx/proxy
sudo chown -R www-data:www-data /var/cache/nginx # nginx:nginx on Rocky/Alma
Microcaché FastCGI para PHP
Cachear la salida de PHP solo 10 segundos puede convertir cientos de ejecuciones de PHP por segundo en una página concurrida en una cada 10 segundos, y el contenido sigue casi en tiempo real:
# /etc/nginx/conf.d/10-fastcgi-cache.conf (http context)
fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=PHPCACHE:32m
max_size=256m inactive=10m use_temp_path=off;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
# Inside the server {} block of the PHP site
set $skip_cache 0;
if ($request_method = POST) { set $skip_cache 1; }
if ($query_string != "") { set $skip_cache 1; }
if ($http_cookie ~* "PHPSESSID|wordpress_logged_in|wp-postpass|comment_author|woocommerce_items_in_cart") { set $skip_cache 1; }
if ($request_uri ~* "^/(wp-admin|wp-login\.php|cart|checkout|my-account)") { set $skip_cache 1; }
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_cache PHPCACHE;
fastcgi_cache_valid 200 301 302 10s;
fastcgi_cache_use_stale error timeout updating http_500 http_503;
fastcgi_cache_background_update on;
fastcgi_cache_lock on;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
add_header X-Cache $upstream_cache_status always;
add_header Alt-Svc 'h3=":443"; ma=86400' always;
}
Cambia el socket según tu versión de PHP: php8.2-fpm en Debian 12, php8.4-fpm en Debian 13, php8.1-fpm en Ubuntu 22.04 y php8.3-fpm en Ubuntu 24.04. En Rocky/Alma, usa include fastcgi_params; con fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; y el socket unix:/run/php-fpm/www.sock. Nunca se cachean los usuarios con sesión iniciada, las peticiones POST, las URL con parámetros, el carrito ni el panel de administración. Además, Nginx no cachea respuestas que establecen una cookie o envían Cache-Control: private o no-store.
$http_cookie y prueba con una sesión iniciada antes de publicar.proxy_cache para una aplicación
# http context, e.g. /etc/nginx/conf.d/10-proxy-cache.conf
proxy_cache_path /var/cache/nginx/proxy levels=1:2 keys_zone=APPCACHE:16m
max_size=512m inactive=60m use_temp_path=off;
# Add inside location / of the upstream example above
proxy_cache APPCACHE;
proxy_cache_key $scheme$host$request_uri;
proxy_cache_valid 200 301 1m;
proxy_cache_valid 404 10s;
proxy_cache_use_stale error timeout updating http_502 http_503 http_504;
proxy_cache_background_update on;
proxy_cache_lock on;
proxy_cache_bypass $http_authorization $cookie_session;
proxy_no_cache $http_authorization $cookie_session;
add_header X-Cache $upstream_cache_status always;
Las peticiones con cabecera Authorization o con la cookie session (cámbiale el nombre según tu aplicación) se saltan la caché. Si la aplicación envía sus propias cabeceras Cache-Control o Expires, Nginx las respeta en lugar de proxy_cache_valid. En Rocky/Alma, SELinux impide que Nginx se conecte a un puerto local y puede bloquear el directorio de caché hasta que ejecutes:
sudo setsebool -P httpd_can_network_connect 1
sudo restorecon -Rv /var/cache/nginx
Paso 8: Prueba y recarga
sudo nginx -t && sudo systemctl reload nginx
curl -sI https://example.com/ | grep -iE 'x-cache|content-encoding|alt-svc'
nginx -t comprueba la sintaxis antes de aplicar nada; reload carga la nueva configuración sin cortar conexiones. Pide dos veces una página cacheada: X-Cache debe pasar de MISS a HIT. Después repite la línea base del paso 1 y compara. Si nginx -t falla, indica el archivo y la línea; corrígelo o restaura tu copia de /root/.
FAQ
¿Qué cambio da la mayor mejora?
En sitios PHP como WordPress, la microcaché FastCGI con diferencia, porque saca a PHP de la mayoría de las peticiones. En sitios estáticos, la compresión y las cabeceras de caché del navegador. Ajustar los workers importa sobre todo cuando tienes muchos visitantes a la vez.
¿Cómo vacío la caché después de actualizar el contenido?
Borra los archivos de caché y recarga: sudo rm -rf /var/cache/nginx/fastcgi/* (o proxy/*) y luego sudo systemctl reload nginx. Con una microcaché de 10 segundos, normalmente basta con esperar.
¿Basta un VPS de 512 MB para Nginx?
Para Nginx en sí, sí: sirve archivos estáticos y páginas cacheadas con muy poca memoria. En el plan mini, el límite suele ser PHP-FPM o la base de datos, así que mantén esos pools pequeños y deja que la caché absorba el tráfico. Si necesitas ayuda, abre un ticket: JUSTG ofrece soporte con IA 24/7 por ticket.