# Iniciar, detener, reiniciar, recargar systemctl start nginx systemctl stop nginx systemctl restart nginx # corta conexiones activas systemctl reload nginx # recarga config sin cortar conexiones # Ver estado systemctl status nginx # Habilitar/deshabilitar inicio automático systemctl enable nginx systemctl disable nginx
Preferí siempre reload sobre restart en producción — no corta las conexiones activas.
# Testear sintaxis ANTES de recargar (obligatorio) nginx -t # Test con detalle nginx -T # Ver versión y módulos compilados nginx -v nginx -V
Siempre ejecutá nginx -t antes de recargar. Un error de sintaxis con restart tira el servidor.
# Activar un sitio (crear symlink en sites-enabled) ln -s /etc/nginx/sites-available/mi-sitio /etc/nginx/sites-enabled/ # Usando la herramienta incluida (si está disponible) nginx_ensite mi-sitio # Desactivar un sitio (borrar solo el symlink, no el archivo) rm /etc/nginx/sites-enabled/mi-sitio # Recargar después de activar/desactivar nginx -t && systemctl reload nginx
# Log de accesos en vivo tail -f /var/log/nginx/access.log # Log de errores (el más útil para debugging) tail -f /var/log/nginx/error.log # Log de un sitio específico (si configuraste logs separados) tail -f /var/log/nginx/mi-sitio.error.log # Filtrar errores recientes grep "error" /var/log/nginx/error.log | tail -50
El flujo es: editar en sites-available, crear symlink en sites-enabled, recargar Nginx.
# /etc/nginx/nginx.conf user www-data; worker_processes auto; # auto = 1 worker por CPU pid /run/nginx.pid; include /etc/nginx/modules-enabled/*.conf; events { worker_connections 1024; } http { # Tipos MIME y charset include /etc/nginx/mime.types; default_type application/octet-stream; charset utf-8; # Logging access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; # Performance sendfile on; tcp_nopush on; keepalive_timeout 65; gzip on; # Cargar todos los sitios activos include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*; }
El bloque mínimo para servir un sitio estático. Archivo en /etc/nginx/sites-available/mi-sitio.conf
server { listen 80; listen [::]:80; # IPv6 server_name ejemplo.com www.ejemplo.com; root /var/www/ejemplo.com/public; index index.html index.htm; location / { try_files $uri $uri/ =404; } # Logs separados por sitio (recomendado) access_log /var/log/nginx/ejemplo.com.access.log; error_log /var/log/nginx/ejemplo.com.error.log; }
# Activar y recargar ln -s /etc/nginx/sites-available/mi-sitio.conf /etc/nginx/sites-enabled/ nginx -t && systemctl reload nginx
Cada subdominio es un bloque server separado con su propio server_name. Archivo en /etc/nginx/sites-available/app.ejemplo.com.conf
server { listen 80; listen [::]:80; server_name app.ejemplo.com; root /var/www/app.ejemplo.com/public; index index.html; location / { try_files $uri $uri/ =404; } access_log /var/log/nginx/app.ejemplo.com.access.log; error_log /var/log/nginx/app.ejemplo.com.error.log; }
El DNS del subdominio debe apuntar a la IP del VPS con un registro tipo A antes de que funcione.
Útil para capturar cualquier subdominio con la misma lógica.
server { listen 80; server_name ~^(?<sub>.+)\.ejemplo\.com$; # regex root /var/www/$sub/public; # carpeta según subdominio index index.html; location / { try_files $uri $uri/ =404; } }
Con wildcard DNS (registro *.ejemplo.com en el DNS) y este bloque, cada subdominio sirve desde su propia carpeta automáticamente.
# www → sin www (301 permanente) server { listen 80; server_name www.ejemplo.com; return 301 https://ejemplo.com$request_uri; } # HTTP → HTTPS server { listen 80; server_name ejemplo.com www.ejemplo.com; return 301 https://$host$request_uri; } # Redirigir una ruta específica location /vieja-ruta { return 301 /nueva-ruta; } # Redirigir con expresión regular rewrite ^/blog/(.*)$ /articulos/$1 permanent;
Configuración completa HTTPS. Certbot puede generarla automáticamente, pero es útil entender su estructura.
# Bloque HTTP → redirige a HTTPS server { listen 80; server_name ejemplo.com www.ejemplo.com; return 301 https://$host$request_uri; } # Bloque HTTPS principal server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name ejemplo.com www.ejemplo.com; # Certificados (generados por Certbot) ssl_certificate /etc/letsencrypt/live/ejemplo.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/ejemplo.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; root /var/www/ejemplo.com/public; index index.html; location / { try_files $uri $uri/ =404; } }
Con Certbot: certbot --nginx -d ejemplo.com -d www.ejemplo.com configura todo esto automáticamente.
Nginx no ejecuta PHP directamente. Pasa las peticiones PHP al proceso PHP-FPM (FastCGI Process Manager) vía socket Unix o TCP. PHP-FPM procesa y devuelve la respuesta a Nginx.
# Instalar PHP-FPM (reemplazar 8.2 por tu versión) apt install php8.2-fpm php8.2-mysql php8.2-curl php8.2-mbstring # Ver estado de PHP-FPM systemctl status php8.2-fpm # Ver versión instalada php -v # Verificar el socket (la ruta que va en nginx) ls /run/php/ # → php8.2-fpm.sock
server { listen 80; server_name mi-php-app.com; root /var/www/mi-php-app/public; index index.php index.html; location / { try_files $uri $uri/ /index.php?$query_string; } # Pasar archivos .php a PHP-FPM location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.2-fpm.sock; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; } # Bloquear acceso a archivos .htaccess (no aplican en Nginx) location ~ /\.ht { deny all; } access_log /var/log/nginx/mi-php-app.access.log; error_log /var/log/nginx/mi-php-app.error.log; }
Frameworks PHP modernos requieren que todas las peticiones pasen por index.php (front controller). El root apunta a la carpeta public/.
server { listen 80; server_name laravel.ejemplo.com; root /var/www/laravel/public; # ← siempre /public index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass unix:/run/php/php8.2-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; fastcgi_hide_header X-Powered-By; } # Archivos estáticos con caché larga location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff2)$ { expires 30d; add_header Cache-Control "public, no-transform"; } location ~ /\.(?!well-known).* { deny all; } access_log /var/log/nginx/laravel.access.log; error_log /var/log/nginx/laravel.error.log; }
server { listen 80; server_name mi-wp.com www.mi-wp.com; root /var/www/mi-wp; index index.php; # Permalinks de WordPress location / { try_files $uri $uri/ /index.php?$args; } # PHP-FPM location ~ \.php$ { fastcgi_pass unix:/run/php/php8.2-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } # Bloquear acceso a wp-config y archivos sensibles location ~* /(wp-config\.php|\.git|readme\.html|license\.txt) { deny all; } # Caché de estáticos location ~* \.(css|js|jpg|jpeg|png|gif|ico|woff2|svg)$ { expires 1y; add_header Cache-Control "public"; log_not_found off; } # Límite de tamaño de subida (sincronizar con php.ini) client_max_body_size 64M; }
Tener un pool de PHP-FPM por sitio aísla los procesos y mejora la seguridad y el monitoreo. Archivo en /etc/php/8.2/fpm/pool.d/mi-sitio.conf
; Pool para mi-sitio (copia de www.conf y adaptá) [mi-sitio] user = www-data group = www-data ; Socket exclusivo para este sitio listen = /run/php/mi-sitio.sock pm = dynamic pm.max_children = 10 pm.start_servers = 2 pm.min_spare_servers = 1 pm.max_spare_servers = 3
# En nginx, usar el socket del pool específico fastcgi_pass unix:/run/php/mi-sitio.sock;
Nginx recibe la petición del cliente y la reenvía a una aplicación corriendo en otro puerto (Node.js, Python, Go, Java, etc.).
server { listen 80; server_name api.ejemplo.com; location / { proxy_pass http://127.0.0.1:3000; # app en puerto 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_cache_bypass $http_upgrade; } }
Los headers X-Real-IP y X-Forwarded-For permiten que tu app vea la IP real del cliente, no la de Nginx.
# Definir el grupo de servidores upstream mi_app { server 127.0.0.1:3000; server 127.0.0.1:3001; server 127.0.0.1:3002; keepalive 32; # conexiones persistentes al upstream } server { listen 80; server_name api.ejemplo.com; location / { proxy_pass http://mi_app; # usa el upstream proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }
Por defecto usa round-robin. Podés agregar least_conn; dentro del upstream para enviar al servidor con menos conexiones activas.
Un mismo dominio sirve el frontend estático y redirige las rutas de la API a la app.
server { listen 80; server_name ejemplo.com; # API → app en Node/Python location /api/ { proxy_pass http://127.0.0.1:4000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # WebSockets location /ws/ { proxy_pass http://127.0.0.1:4000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; } # Frontend → archivos estáticos location / { root /var/www/ejemplo.com/dist; try_files $uri $uri/ /index.html; # SPA fallback } }
Gunicorn es el servidor WSGI que corre la app Django. Nginx actúa como proxy inverso delante de él.
# 1. Instalar Gunicorn en el entorno virtual pip install gunicorn # 2. Iniciar Gunicorn manualmente (para probar) gunicorn --workers 3 --bind 127.0.0.1:8000 mi_proyecto.wsgi:application # 3. Crear servicio systemd para que arranque solo # /etc/systemd/system/gunicorn.service
# /etc/systemd/system/gunicorn.service [Unit] Description=Gunicorn daemon para Django After=network.target [Service] User=www-data Group=www-data WorkingDirectory=/var/www/mi-django ExecStart=/var/www/mi-django/venv/bin/gunicorn \ --workers 3 \ --bind unix:/run/gunicorn/mi-django.sock \ mi_proyecto.wsgi:application [Install] WantedBy=multi-user.target
systemctl enable gunicorn
systemctl start gunicorn
# Nginx → Gunicorn (via socket Unix) server { listen 80; server_name mi-django.com; # Archivos estáticos servidos directamente por Nginx location /static/ { alias /var/www/mi-django/staticfiles/; } location /media/ { alias /var/www/mi-django/media/; } # Todo lo demás → Gunicorn location / { proxy_pass http://unix:/run/gunicorn/mi-django.sock; 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; } }
FastAPI y apps ASGI usan Uvicorn (o Hypercorn). El patrón es idéntico al de Gunicorn.
# Instalar pip install uvicorn gunicorn # Para FastAPI con Uvicorn workers gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app --bind 127.0.0.1:8001 # Para Flask con Gunicorn gunicorn -w 4 "app:create_app()" --bind 127.0.0.1:8001
# Nginx → FastAPI / Flask server { listen 80; server_name api.ejemplo.com; location / { proxy_pass http://127.0.0.1:8001; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; } }
PM2 mantiene el proceso Node vivo y lo reinicia si falla. Nginx hace el proxy adelante.
# Instalar PM2 globalmente npm install -g pm2 # Iniciar la app pm2 start server.js --name "mi-app" # Para Next.js pm2 start npm --name "nextjs" -- start # Guardar configuración y activar al inicio pm2 save pm2 startup # Comandos útiles de PM2 pm2 list # ver apps corriendo pm2 logs mi-app # ver logs pm2 restart mi-app # reiniciar pm2 stop mi-app # detener pm2 monit # monitor interactivo
# Nginx → Node.js / Next.js server { listen 80; server_name mi-node-app.com; 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_read_timeout 300s; proxy_cache_bypass $http_upgrade; } }
Cuando la app corre en Docker, Nginx proxy al puerto mapeado del contenedor.
# El contenedor expone su puerto al host # docker run -p 127.0.0.1:8080:80 mi-app # ↑ solo en loopback, Nginx hace el proxy público server { listen 80; server_name docker-app.ejemplo.com; location / { proxy_pass http://127.0.0.1:8080; # puerto del contenedor 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; } }
Mapear el contenedor a 127.0.0.1:puerto (no solo :puerto) evita que el puerto quede expuesto públicamente; solo Nginx puede accederlo.
Agregar estos headers en el bloque server o en nginx.conf dentro de http {} para que apliquen a todos los sitios.
# Ocultar versión de Nginx server_tokens off; # Protección contra clickjacking add_header X-Frame-Options "SAMEORIGIN" always; # Protección XSS add_header X-XSS-Protection "1; mode=block" always; # Evitar sniffing de MIME add_header X-Content-Type-Options "nosniff" always; # HTTPS estricto (solo en sitios con SSL) add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # Política de referrer add_header Referrer-Policy "no-referrer-when-downgrade" always; # Content Security Policy (adaptar a tu app) add_header Content-Security-Policy "default-src 'self'; script-src 'self'" always;
# Definir zona de límite en http{} de nginx.conf limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m; # Aplicar en el server block location /api/ { limit_req zone=api_limit burst=20 nodelay; proxy_pass http://127.0.0.1:3000; } # Más estricto para el login location /login { limit_req zone=login_limit burst=3; proxy_pass http://127.0.0.1:3000; }
burst permite picos temporales. nodelay procesa el burst inmediatamente en vez de poner en cola.
# Permitir solo ciertas IPs (ej: panel de admin) location /admin { allow 192.168.1.100; # tu IP allow 10.0.0.0/8; # red interna deny all; proxy_pass http://127.0.0.1:3000; } # Autenticación HTTP básica location /privado { auth_basic "Área restringida"; auth_basic_user_file /etc/nginx/.htpasswd; }
# Crear archivo de contraseñas apt install apache2-utils htpasswd -c /etc/nginx/.htpasswd mi_usuario
# Bloquear user-agents maliciosos map $http_user_agent $blocked_agent { default 0; ~*malicious 1; ~*scanner 1; ~*harvester 1; } server { if ($blocked_agent) { return 403; } # Ocultar archivos ocultos (.env, .git, etc.) location ~ /\. { deny all; access_log off; log_not_found off; } # Bloquear acceso a archivos de configuración location ~* \.(env|log|conf|ini|sh|sql|bak)$ { deny all; } }
# En el bloque http{} de nginx.conf gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 6; # 1-9, balance CPU/compresión gzip_min_length 1024; # no comprimir respuestas pequeñas gzip_buffers 16 8k; gzip_http_version 1.1; gzip_types text/plain text/css text/javascript application/javascript application/json application/xml image/svg+xml font/woff2;
# Caché larga para assets con hash en el nombre location ~* \.(css|js|woff2|woff|ttf)$ { expires 1y; add_header Cache-Control "public, immutable"; } # Caché media para imágenes location ~* \.(jpg|jpeg|png|gif|ico|svg|webp)$ { expires 30d; add_header Cache-Control "public, no-transform"; log_not_found off; } # Sin caché para HTML y JSON (contenido dinámico) location ~* \.(html|htm)$ { expires -1; add_header Cache-Control "no-cache, no-store, must-revalidate"; }
# En http{} de nginx.conf # Timeouts de conexión client_body_timeout 30s; client_header_timeout 30s; keepalive_timeout 65s; send_timeout 30s; # Límite de tamaño de body (subida de archivos) client_max_body_size 32M; # Buffers client_body_buffer_size 128k; client_header_buffer_size 1k; large_client_header_buffers 4 8k; # Proxy timeouts (para apps lentas) proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s;
# Definir zona de caché en http{} proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=mi_cache:10m max_size=1g inactive=60m use_temp_path=off; # Usar caché en el server block location /api/ { proxy_cache mi_cache; proxy_cache_valid 200 10m; # cachear 200 por 10 min proxy_cache_valid 404 1m; proxy_cache_use_stale error timeout updating; proxy_cache_lock on; add_header X-Cache-Status $upstream_cache_status; proxy_pass http://127.0.0.1:3000; }
El header X-Cache-Status muestra si la respuesta fue HIT, MISS o BYPASS — útil para debugging.
Guardá configuración repetida en un snippet e incluilo desde cualquier server block. Archivo en /etc/nginx/snippets/proxy-params.conf
# /etc/nginx/snippets/proxy-params.conf proxy_http_version 1.1; proxy_cache_bypass $http_upgrade; 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;
# Usarlo en cualquier location location / { include snippets/proxy-params.conf; proxy_pass http://127.0.0.1:3000; }