Nginx en EGPHP
La puerta de entrada, bien construida.
Nginx decide qué llega a su sitio: qué servidor y qué pool de PHP responden, qué sale de la caché y qué se rechaza en la puerta. Así lo monta nuestro equipo, con la configuración a la vista.
NGINX
PHP
Redis
Una petición, de la puerta a su código
La mayoría de las peticiones no necesitan PHP en absoluto. Cada parada de abajo puede responder por sí sola, y solo lo que queda sigue hasta la siguiente.
-
La conexión
Cifrada con TLS 1.3, y una sola conexión transporta todos los archivos de la página por HTTP/2 o HTTP/3.
-
Límites
Aquí se responde a demasiadas peticiones desde una misma dirección, a demasiadas conexiones abiertas o a un cuerpo mayor del que acepta el sitio.
429 o 413: se detiene aquí -
Rutas prohibidas
Los archivos ocultos, las copias de seguridad y cualquier script dentro de una carpeta de subidas se rechazan antes de que PHP se entere.
403 o 404: se detiene aquí -
Archivos estáticos
Imágenes, estilos y scripts salen directamente del disco, con cachés de larga duración.
Servido: se detiene aquí -
Caché de páginas
Un visitante sin sesión iniciada recibe una página terminada desde la memoria.
Acierto de caché: se detiene aquí -
El balanceador
Elige el servidor y el pool de PHP que tienen hueco para la petición.
-
El pool propio de la cuenta
PHP se ejecuta como el usuario propio de la cuenta, dentro de sus propias carpetas y con su propio php.ini.
Balanceo de carga entre servidores
Cuando un servidor no basta, Nginx se coloca delante de varios y reparte el tráfico entre ellos. El método decide quién recibe la siguiente petición.
El método por defecto. Cada servidor recibe la siguiente petición por turnos: lo adecuado cuando los servidores son parecidos y las peticiones cuestan más o menos lo mismo.
La siguiente petición va al servidor con menos conexiones abiertas: lo adecuado cuando algunas peticiones son largas, como informes, exportaciones y subidas.
La dirección del visitante elige el servidor, así que el mismo visitante siempre cae en el mismo. Sirve para aplicaciones que guardan las sesiones en el disco local; mejor aún, llevamos las sesiones a Redis y la necesidad desaparece.
upstream app_servers {
# round robin: the default, no line needed least_conn; ip_hash;
server 10.0.0.11:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.12:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.13:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.14:8080 backup; # no backup server with ip_hash
keepalive 32;
}
server {
server_name example.com;
location / {
proxy_pass http://app_servers;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_next_upstream error timeout http_502 http_503;
}
}
- max_fails · fail_timeout
- Comprobaciones de salud con tráfico real: tras tres intentos fallidos en treinta segundos, el servidor queda fuera durante treinta segundos y luego se vuelve a probar.
- backup
- Un servidor de reserva que solo recibe tráfico cuando ningún otro servidor está disponible.
- proxy_next_upstream
- Una petición que se encuentra con un error o un tiempo de espera agotado se reintenta en el siguiente servidor. Una petición que modifica datos no se envía dos veces, así que un pedido nunca se hace dos veces.
- keepalive
- Las conexiones con los servidores se mantienen abiertas entre peticiones en lugar de abrirse de nuevo para cada una.
- weight
- A un servidor más grande se le puede asignar una parte mayor de las peticiones.
- down
- Un servidor se retira para mantenimiento con una recarga, mientras los demás llevan el tráfico.
Balanceo de carga entre pools de PHP
La misma idea dentro de un solo servidor. Un sitio puede tener más de un pool de PHP-FPM detrás, de modo que un pool puede reiniciarse mientras el otro responde, y el trabajo lento nunca se queda con los workers que necesita la tienda.
~ \.php$^~ /admin/
shop-a.sock
Tienda, pool A
Respondiendo
Respondiendo y asumiendo las peticiones de B
shop-b.sock
Tienda, pool B
Respondiendo
Reiniciándose con un ajuste nuevo: ningún visitante lo espera
shop-admin.sock
Administración e informes
Sus propios workers, aparte de la tienda
upstream php_shop {
server unix:/run/php/shop-a.sock max_fails=2 fail_timeout=10s;
server unix:/run/php/shop-b.sock max_fails=2 fail_timeout=10s;
keepalive 8;
}
# server {}
location ~ \.php$ {
try_files $uri =404;
include fastcgi.conf;
fastcgi_pass php_shop;
fastcgi_keep_conn on;
fastcgi_next_upstream error timeout;
}
location ^~ /admin/ {
location ~ \.php$ {
try_files $uri =404;
include fastcgi.conf;
fastcgi_pass unix:/run/php/shop-admin.sock;
}
}
-
Dos pools, un sitio
Los dos pools ejecutan el mismo código con la misma cuenta. Un ajuste nuevo de PHP llega a un pool y luego al otro, y los visitantes siguen recibiendo respuesta en todo momento.
-
Conmutación por error
fastcgi_next_upstreampasa una petición al otro pool cuando su socket la rechaza o no responde a tiempo. Una petición que modifica datos no se envía dos veces. -
Un carril para el trabajo lento
El área de administración, las exportaciones y los informes tienen su propio pool, así que un informe de una hora no puede quedarse con los workers que un cliente necesita para pagar.
- Cómo dimensionamos un pool de PHP-FPM PHP en EGPHP
Las reglas de la puerta
La protección empieza antes de que PHP ejecute una sola línea. Estas reglas van en cada bloque server que escribimos, adaptadas al sitio.
# http {}
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_conn_zone $binary_remote_addr zone=conns:10m;server {
listen 443 ssl;
http2 on;
server_name example.com;
limit_req zone=perip burst=40 nodelay;
limit_conn conns 30;
limit_req_status 429;
location = /wp-login.php {
limit_req zone=login burst=5 nodelay;
include fastcgi.conf;
fastcgi_pass php_shop;
} client_max_body_size 64m;
client_header_timeout 15s;
client_body_timeout 15s; location ~ /\.(?!well-known/) { deny all; }
location ~* \.(?:bak|old|orig|sql|swp)$ { deny all; } location ~* ^/(?:wp-content/uploads|uploads|files)/.*\.php$ {
deny all;
} location ~ \.php$ {
try_files $uri =404;
include fastcgi.conf;
fastcgi_pass php_shop;
} ssl_protocols TLSv1.2 TLSv1.3;
server_tokens off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
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;}
-
Límites de peticiones y de conexiones
Cada dirección tiene un ritmo constante con margen para una ráfaga corta; si lo supera, Nginx responde 429 y PHP ni se despierta. La página de inicio de sesión tiene su propio límite, mucho más estricto, que deja sin fuelle los intentos de adivinar contraseñas.
-
Tamaño del cuerpo y tiempos de espera
Una subida mayor de la que acepta el sitio se rechaza al instante, y un cliente que envía su petición muy despacio queda desconectado en lugar de mantener una conexión abierta.
-
Archivos ocultos y restos
Nunca se sirve nada cuyo nombre empiece por un punto, como
.gito.env, ni las copias de seguridad de los archivos. La única excepción es la carpeta que usan las comprobaciones de certificados. -
Las subidas nunca ejecutan PHP
Una carpeta que acepta archivos de los visitantes los sirve como archivos. Un script colocado ahí se rechaza, se llame como se llame.
-
Solo un archivo PHP que exista
try_files $uri =404envía una petición a PHP solo si ese archivo exacto está en el disco. Así se cierra la trampa clásica que se dibuja abajo. -
Cabeceras y TLS
HSTS mantiene a los navegadores en HTTPS, las demás cabeceras impiden que se husmee el tipo de una página o que otro sitio la meta en un marco, y el servidor no anuncia su versión. Solo TLS 1.2 y 1.3.
Cómo protegemos el propio servidor
La trampa que abre una sola línea que falta
/uploads/avatar.jpgUn archivo real: una imagen con código PHP escondido dentro
/shell.phpNo existe
En una configuración antigua o descuidada, PHP recorre la ruta hacia atrás, encuentra la imagen y ejecuta el código que lleva dentro.
Un 404, y no se ejecuta nada. cgi.fix_pathinfo=0 en PHP y security.limit_extensions en el pool son la segunda y la tercera cerradura de la misma puerta.
Cada cuenta es una habitación amurallada
En un servidor compartido por varios clientes, un sitio nunca debe poder leer a otro. Levantamos ese muro por capas, para que ningún ajuste lo sostenga por sí solo.
shopCuenta- Usuario de Linux
- shop
- Pool de PHP
- /run/php/shop.sock
- Carpetas permitidas
- /home/shop
- Ajustes
- pool.d/shop.conf
clinicCuenta- Usuario de Linux
- clinic
- Pool de PHP
- /run/php/clinic.sock
- Carpetas permitidas
- /home/clinic
- Ajustes
- pool.d/clinic.conf
blogCuenta- Usuario de Linux
- blog
- Pool de PHP
- /run/php/blog.sock
- Carpetas permitidas
- /home/blog
- Ajustes
- pool.d/blog.conf
file_get_contents('/home/clinic/public_html/config.php')
Permission deniedopen_basedir restriction in effect
-
Su propio usuario de Linux
Cada sitio se ejecuta con su propio usuario, y cada carpeta personal está cerrada a las demás cuentas. Este es el muro que aguanta incluso si PHP queda fuera de la ecuación.
-
Su propio pool de PHP-FPM
Un pool por cuenta, accesible solo a través de su propio socket. Un sitio saturado o averiado agota sus propios workers, no los de sus vecinos.
-
open_basediryinclude_pathPHP solo puede abrir archivos dentro de las carpetas de la propia cuenta, y solo busca ahí los archivos incluidos. Un script que pide una ruta fuera de ellas se rechaza antes de tocar el disco.
-
Sus propios archivos temporales y sesiones
Las subidas en curso y los archivos de sesión se quedan dentro de la cuenta, no en un
/tmpcompartido donde cualquier sitio podría leer las sesiones iniciadas de otro. -
Ninguna función que el sitio no necesite
open_basedirno es un entorno aislado: no detiene un programa lanzado desde PHP. Por eso las funciones que ejecutan comandos de shell se desactivan en cada cuenta, salvo que el sitio de verdad necesite alguna.
[shop]
user = shop
group = shop
listen = /run/php/shop.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = ondemand
pm.max_children = 12
pm.process_idle_timeout = 10s
php_admin_value[open_basedir] = /home/shop/public_html:/home/shop/lib:/home/shop/tmp
php_admin_value[include_path] = .:/home/shop/lib
php_admin_value[upload_tmp_dir] = /home/shop/tmp
php_admin_value[sys_temp_dir] = /home/shop/tmp
php_admin_value[session.save_path] = /home/shop/tmp/sessions
php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen
php_admin_flag[allow_url_include] = off
php.ini por cuenta
Cada cuenta tiene sus propios ajustes, escritos en su propio pool. Algunos los fijamos nosotros, otros puede cambiarlos el sitio, y la diferencia es una sola palabra.
php_admin_value: fijo
Fijado para la cuenta. Ni ini_set() ni un archivo .user.ini pueden cambiarlo.
memory_limit = 256M
max_execution_time = 60
open_basedir = /home/shop/…
disable_functions = exec,…
php_value: un valor inicial
Nuestro valor por defecto para la cuenta. El sitio puede subirlo o bajarlo donde PHP lo permita.
upload_max_filesize = 32M
post_max_size = 32M
max_input_vars = 3000
.user.ini: el del propio sitio
Un archivo en la carpeta del sitio para los ajustes que este puede cambiar. PHP lo vuelve a leer cada cinco minutos por defecto.
upload_max_filesize = 64M
post_max_size = 64M
display_errors = Off
Un ejemplo: los valores se fijan para cada sitio
Límites que tienen que coincidir
upload_max_filesizePHP≤
post_max_sizePHP≤
client_max_body_sizeNginx
Si Nginx permite menos, la subida se rechaza con un 413 antes de que PHP la vea; si PHP permite menos, el archivo llega y se descarta.
max_execution_timePHP<
request_terminate_timeoutPHP-FPM≤
fastcgi_read_timeoutNginx
PHP debería detenerse primero, con un error que pueda registrar; el pool detiene lo que PHP no pudo detener; y Nginx espera lo suficiente para no mostrar un 504 mientras PHP sigue trabajando.
Caché por capas
Cada capa guarda algo distinto, y una petición solo baja hasta donde lo necesita. Cambie entre los dos visitantes para ver dónde se detiene cada uno.
-
NavegadorEstilos, scripts e imágenes con una versión en el nombre, guardados durante un año.Ya tiene los archivosYa tiene los archivos
-
Caché de páginas de NginxPáginas terminadas, guardadas desde unos segundos hasta unos minutos.Acierto: la página se responde aquíOmitida: una cookie de sesión o de carrito indica que la página es personal
-
OPcachePHP compilado una vez y guardado en memoria.No se llega hasta aquíEl código se ejecuta sin volver a leerse ni compilarse
-
RedisRespuestas repetidas de la base de datos, y sesiones.No se llega hasta aquíLa mayoría de las respuestas salen de la memoria
-
Base de datosLa fuente de la verdad.No se llega hasta aquíSolo se le pregunta lo que Redis no tenía
# http {}
fastcgi_cache_path /var/cache/nginx/shop levels=1:2
keys_zone=shop:50m inactive=60m max_size=2g;
map $http_cookie $skip_cache {
default 0;
~wordpress_logged_in_ 1;
~woocommerce_items_in_cart 1;
}
# location ~ \.php$ {}
fastcgi_cache shop;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_valid 200 301 10s;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache_use_stale updating error timeout http_500 http_503;
fastcgi_cache_background_update on;
fastcgi_cache_lock on;
- La clave
- Esquema, método, host y la dirección completa, para que dos páginas distintas nunca compartan una entrada.
- Saltarse la caché
- Una sesión iniciada o un carrito se saltan la caché en los dos sentidos: la página ni se sirve desde ella ni se guarda en ella. Los formularios enviados con
POSTnunca se guardan en caché. - Copia antigua mientras se actualiza
- Cuando una entrada caduca, una sola petición la renueva en segundo plano mientras todos los demás siguen recibiendo la copia anterior. Si PHP falla, se sirve la última página buena.
- Una regla que se suele pasar por alto
- Una directiva
add_headerdentro de un location sustituye todas las cabeceras definidas por encima, así que mantenemos las cabeceras de seguridad en un solo fragmento y lo incluimos allí donde se añaden cabeceras.
Velocidad, y el resto de una buena configuración
Los ajustes que hacen más rápido cada sitio del servidor, y los hábitos que mantienen seguro un cambio.
$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
$ sudo systemctl reload nginx
La nueva configuración se prueba primero; si la prueba falla, no cambia nada. Al recargar, los workers nuevos arrancan con ella mientras los antiguos terminan las peticiones que tienen en curso, así que no se corta a ningún visitante.
log_format timed '$remote_addr "$request" $status $body_bytes_sent '
'rt=$request_time urt=$upstream_response_time '
'cache=$upstream_cache_status';
Cada línea del registro anota el tiempo total, el tiempo que tardó PHP y lo que hizo la caché, así que una página lenta se rastrea hasta Nginx, PHP o la red en lugar de adivinarse.
- http2 on; listen 443 quic;
- Una sola conexión transporta toda la página; HTTP/3 se añade donde la compilación y la red lo permiten.
- gzip · brotli
- El texto se comprime antes de salir, con Brotli cuando el módulo está compilado. Las imágenes y el vídeo no se comprimen dos veces.
- sendfile · tcp_nopush
- El núcleo envía los archivos del disco a la red por sí mismo, en paquetes completos.
- open_file_cache
- Nginx recuerda qué archivos existen y dónde están, en lugar de preguntar al disco en cada petición.
- worker_processes auto
- Un worker por núcleo de procesador, cada uno con muchas conexiones en un solo bucle de eventos.
- Cache-Control: immutable
- Los archivos con una versión en el nombre se guardan en caché un año y nunca se vuelven a comprobar.
Qué hay detrás de la puerta de entrada
Agrupado según el trabajo de cada pieza, todo decidido en la configuración de Nginx.
Conexiones
- Conexiones cifradasTLS 1.3 cifra la conexión y HTTP/2 lleva todos los archivos de una página por una sola conexión.
- Límites de frecuencia y tamañoDemasiadas peticiones, demasiadas conexiones abiertas o un cuerpo demasiado grande se responden en la puerta.
Entrega
- Archivos estáticosImágenes, estilos y scripts salen directo del disco, con largos tiempos de caché.
- Caché de páginasUn visitante sin sesión iniciada recibe una página terminada desde la memoria.
- Balanceo de cargaLas peticiones van al servidor y al pool de PHP que tienen sitio para ellas.
Protección
- Rutas prohibidasLos archivos ocultos, las copias y los scripts en carpetas de subida se rechazan antes de que PHP sepa de ellos.
- Cabeceras de seguridadSe guardan en un solo fragmento y se incluyen donde se añadan cabeceras.
- Cuentas amuralladasCada cuenta ejecuta PHP como su propio usuario, dentro de sus propias carpetas.
Cambiar la configuración sin soltar a ningún visitante
Nginx aplica una configuración nueva sobre la marcha. Así describe su documentación el orden de los hechos.
- Probar el archivo
El comando nginx -t lee toda la configuración y se detiene en el primer error, antes de que nada funcione con ella.
- Enviar la señal de recarga
El proceso maestro recibe la señal y comprueba la nueva configuración. Si falla, revierte y la configuración anterior sigue sirviendo.
- Arrancan workers nuevos
Si la comprobación pasa, el maestro arranca nuevos procesos worker que usan la nueva configuración.
- Los workers viejos terminan
Los workers viejos dejan de aceptar conexiones nuevas y terminan las peticiones que ya tienen.
- Los workers viejos salen
Cuando se sirve al último visitante se apagan y solo quedan workers con la nueva configuración.

Tráiganos el sitio. Nosotros construiremos su puerta de entrada.
Cuéntenos dónde funciona hoy y qué necesita. Primero revisamos la configuración actual, luego proponemos la configuración de Nginx y PHP, y presupuestamos el trabajo después de esa conversación.