Tu página carga. Hasta que deja de hacerlo. Entonces nginx lanza un 502 y tu canal de incidentes se ilumina como un árbol de Navidad hecho de fatiga de on-call.
Reinicias cosas. A veces “funciona”. A veces no. El balanceador de carga sigue reintentando, que es su forma de ayudarte a sufrir más.
En Debian 13 aparece con frecuencia un modo de fallo muy específico: PHP-FPM está sano, nginx está sano, pero el socket Unix entre ambos
no lo está. No está roto. No falta. Simplemente… no es accesible. Un arreglo de una línea en permisos puede acabar con horas de investigación—si entiendes qué estás arreglando realmente.
El expediente: qué significa realmente “permisos del socket”
Cuando nginx habla con PHP-FPM, normalmente lo hace de dos maneras: TCP (127.0.0.1:9000) o un socket de dominio Unix
(algo como /run/php/php8.3-fpm.sock). Los sockets Unix son más rápidos de conectar, evitan exposición de puertos y
son sencillos de razonar en un único host. Hasta que no lo son.
Un socket Unix es un objeto de sistema de ficheros. Eso significa que se aplican permisos de Linux. Y porque está bajo /run,
reside en tmpfs: se recrea al arrancar, a menudo se recrea al iniciar un servicio y a veces systemd “útilmente” lo recrea.
No “haces chmod al socket una vez” y asunto arreglado. Debes asegurarte de que el propietario/grupo/modo correctos existan cada vez que se cree el socket.
El punto doloroso recurrente en Debian 13: los paquetes son razonables, pero tu configuración local (o una cookbook antigua)
asume que el socket pertenece a www-data:www-data con modo 0660. Mientras tanto, el worker de nginx corre como
www-data pero PHP-FPM puede estar configurado distinto, o los permisos del directorio que contiene el socket no permiten la traversión,
o systemd compite contigo con un valor por defecto que revierte permisos después de reinicios.
El resultado final suele ser uno de estos:
connect() to unix:/run/php/php8.3-fpm.sock failed (13: Permission denied)connect() to unix:/run/php/php8.3-fpm.sock failed (2: No such file or directory)upstream prematurely closed connection(a menudo es otro problema, pero puede estar adyacente a permisos durante churn)
Nuestra tarea es convertir esas cadenas en un árbol de decisiones, no en una sesión de espiritismo.
Guion de diagnóstico rápido (comprobar 1/2/3)
1) Confirma que nginx falla al conectar a un socket Unix (no que PHP esté caído)
La primera pregunta siempre es: “¿Es un problema de transporte o de aplicación?”
Si nginx no puede conectar al socket, PHP ni siquiera tiene la oportunidad de decepcionarte.
2) Inspecciona el objeto socket y su directorio padre
Si el socket existe, comprueba propietario/modo. Luego revisa los permisos del directorio. Linux necesita el bit de ejecución (“traversión”) en cada
componente del directorio padre, no solo permisos de lectura en el archivo socket.
3) Verifica los ajustes del pool de PHP-FPM que controlan la creación del socket
Los permisos del socket no se fijan por arte de magia. Provienen de la configuración del pool: listen, listen.owner,
listen.group, listen.mode. Si no están establecidos, se aplican valores por defecto—a veces no los que piensas.
4) Solo entonces: comprueba AppArmor/SELinux y systemd tmpfiles
En el mundo Debian, AppArmor es más común que SELinux. Pero no empieces por ahí.
Empieza por lo mundano. La mayoría de los 502 son aburridos, y lo aburrido se arregla.
Hechos y contexto: por qué Debian 13 hace que esto parezca nuevo
Unos cuantos hechos concretos y puntos de contexto histórico ayudan a explicar por qué esta “pequeña solución” sigue mordiendo a los equipos durante actualizaciones y migraciones:
- Los sockets de dominio Unix preceden a la mayoría de las pilas web. Proceden de diseños de IPC de Unix y se comportan como objetos de sistema de ficheros, no como puertos de red.
/runreemplazó a/var/runen las distribuciones modernas. Es un tmpfs pensado para estado en tiempo de ejecución. Excelente para limpieza; molesto para la costumbre de “lo chmodée una vez”.- Debian estandarizó las ubicaciones de socket de PHP-FPM. La ruta típica
/run/php/phpX.Y-fpm.sockes consistente, pero la consistencia expone drift: configuraciones antiguas de nginx siguen apuntando a rutas previas. - Los pools de PHP-FPM controlan el modo del socket en el momento de creación. Una vez creado, cambios externos en permisos son efímeros: reiniciar el servicio recrea el socket y tus cambios manuales desaparecen.
- nginx y PHP-FPM a menudo ejecutan bajo usuarios distintos por buenas razones. Puedes ejecutar nginx como
www-datapero pools de PHP-FPM como usuarios por sitio para aislamiento; el socket necesita un grupo compartido o una estrategia de ACL. - systemd cambió la historia de la “propiedad en arranque”. tmpfiles y el sandboxing de unidades pueden imponer reglas en el sistema de ficheros que desconocías, especialmente cuando usas overrides.
- Los perfiles de AppArmor son más comunes y estrictos. Un socket puede tener permisos perfectos y aun así ser bloqueado por una política de confinamiento, especialmente con rutas personalizadas.
- 502 es un error paraguas de nginx. No significa “PHP está caído.” Significa “upstream falló.” A veces el upstream es solo “un socket que no puedes abrir.”
- Los errores de permisos son deterministas pero parecen intermitentes bajo carga. Si tienes múltiples workers de nginx, reinicios escalonados o pools múltiples, puedes obtener “a veces funciona” cuando en realidad estás golpeando distintos upstreams.
Una idea parafraseada de Gene Kim (autor sobre DevOps/reliability): Mejorar el flujo significa eliminar pequeñas restricciones que silenciosamente estrangulan todo el sistema.
Los permisos de socket son una restricción pequeña con un gran radio de impacto.
Broma #1: Los sockets Unix son como las puertas de una oficina—si no estás en la lista de credenciales correcta, serás “bad gateway” para todos adentro.
Cómo se presentan realmente los 502 por sockets de PHP-FPM
Los problemas de permisos de socket rara vez aparecen como un limpio banner de “permiso denegado” en el navegador.
Aparecen como 502, picos aleatorios de latencia (por reintentos) y mucho tiempo perdido mirando código PHP que nunca se ejecutó.
Las tres formas comunes
-
Permiso denegado (errno 13): nginx puede ver la ruta del socket, pero no puede abrirlo. Típicamente propietario/grupo/modo incorrectos,
o falta del bit de ejecución en el directorio que contiene el socket. -
No existe el archivo o directorio (errno 2): nginx apunta a una ruta de socket que no existe (versión equivocada, nombre de pool erróneo),
o PHP-FPM no lo creó (no arrancó,listenmal configurado, o falta el directorio). -
Conexión rechazada / upstream timed out: puede ocurrir con backends TCP o PHP-FPM sobrecargado. Con sockets, suele ser
agotamiento de backlog o PHP-FPM no aceptando conexiones lo bastante rápido.
Por qué “funcionó ayer”
Si tu socket está bajo /run, un reboot lo resetea. Un reinicio del servicio lo recrea. Una actualización de paquete puede recargar unidades.
Si tu arreglo fue manual (chmod en el socket), nunca fue una solución; fue una tregua temporal.
Tareas de campo: comandos, salidas y decisiones (12+)
Estas son las tareas que ejecuto cuando alguien dice “nginx 502 después de actualizar a Debian 13” y el error huele a sockets.
Cada tarea incluye un comando, cómo suele verse la salida y qué decisión tomo según ella.
Task 1: Confirma la cadena exacta de error de nginx
cr0x@server:~$ sudo tail -n 30 /var/log/nginx/error.log
2025/12/30 09:21:44 [crit] 1842#1842: *912 connect() to unix:/run/php/php8.3-fpm.sock failed (13: Permission denied) while connecting to upstream, client: 203.0.113.10, server: example.internal, request: "GET /index.php HTTP/1.1", upstream: "fastcgi://unix:/run/php/php8.3-fpm.sock:", host: "example.internal"
Qué significa: nginx alcanzó la ruta del sistema de ficheros, intentó conectar y el kernel dijo “no”.
Decisión: deja de mirar el código PHP; empieza a inspeccionar la propiedad del socket y los permisos del directorio.
Task 2: Valida qué upstream cree nginx que es
cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -R --line-number -E 'fastcgi_pass|upstream' /etc/nginx | head
/etc/nginx/sites-enabled/app.conf:24: fastcgi_pass unix:/run/php/php8.3-fpm.sock;
Qué significa: La configuración activa apunta a ese socket.
Decisión: si la ruta está equivocada (versión PHP antigua, pool equivocado), corrige nginx primero. Si está bien, sigue.
Task 3: Comprueba que PHP-FPM está corriendo y qué unidad es relevante
cr0x@server:~$ systemctl status php8.3-fpm --no-pager
● php8.3-fpm.service - The PHP 8.3 FastCGI Process Manager
Loaded: loaded (/lib/systemd/system/php8.3-fpm.service; enabled; preset: enabled)
Active: active (running) since Tue 2025-12-30 09:10:03 UTC; 12min ago
Docs: man:php-fpm8.3(8)
Process: 1024 ExecStartPost=/usr/lib/php/php-fpm-socket-helper install /run/php/php8.3-fpm.sock /etc/php/8.3/fpm/pool.d/www.conf 83 (code=exited, status=0/SUCCESS)
Main PID: 1012 (php-fpm8.3)
Tasks: 8 (limit: 18925)
Memory: 44.2M
CPU: 2.021s
CGroup: /system.slice/php8.3-fpm.service
├─1012 "php-fpm: master process (/etc/php/8.3/fpm/php-fpm.conf)"
├─1018 "php-fpm: pool www"
└─1019 "php-fpm: pool www"
Qué significa: El servicio está activo; incluso ejecutó un helper post-start relacionado con sockets.
Decisión: la falla del upstream probablemente es de permiso/ruta, no de “php-fpm está muerto”.
Task 4: Inspecciona el objeto socket en sí
cr0x@server:~$ sudo ls -l /run/php/php8.3-fpm.sock
srw-rw---- 1 root root 0 Dec 30 09:10 /run/php/php8.3-fpm.sock
Qué significa: El socket pertenece a root:root con modo 660.
nginx ejecutándose como www-data no puede conectar a menos que esté en el grupo root (no debería).
Decisión: establece correctamente listen.owner/listen.group (o usa ACL) para que el socket sea accesible.
Task 5: Confirma el usuario de los workers de nginx
cr0x@server:~$ ps -o user,group,comm -C nginx
USER GROUP COMMAND
root root nginx
www-data www-data nginx
www-data www-data nginx
www-data www-data nginx
Qué significa: Los workers corren como www-data.
Decisión: el socket debe ser accesible por www-data (como usuario o grupo), o necesitas cambiar intencionalmente el usuario de nginx.
Task 6: Comprueba permisos del directorio /run/php
cr0x@server:~$ sudo namei -l /run/php/php8.3-fpm.sock
f: /run/php/php8.3-fpm.sock
drwxr-xr-x root root /
drwxr-xr-x root root run
drwxr-x--- root www-data php
srw-rw---- root root php8.3-fpm.sock
Qué significa: nginx (www-data) puede traversar /run/php porque el grupo es www-data y el modo incluye x.
Pero el socket en sí es root:root, así que aún falla.
Decisión: arregla la propiedad/modo del socket, no el directorio (el directorio ya es razonable aquí).
Task 7: Localiza la configuración del pool de PHP-FPM que define listen
cr0x@server:~$ sudo grep -R --line-number -E '^listen(\.| =)|^user =|^group =' /etc/php/8.3/fpm/pool.d
/etc/php/8.3/fpm/pool.d/www.conf:31:user = www-data
/etc/php/8.3/fpm/pool.d/www.conf:32:group = www-data
/etc/php/8.3/fpm/pool.d/www.conf:41:listen = /run/php/php8.3-fpm.sock
/etc/php/8.3/fpm/pool.d/www.conf:42:listen.owner = root
/etc/php/8.3/fpm/pool.d/www.conf:43:listen.group = root
/etc/php/8.3/fpm/pool.d/www.conf:44:listen.mode = 0660
Qué significa: El pool crea explícitamente un socket propiedad de root. Ese es el bug, no un misterio.
Decisión: establece listen.owner/listen.group a un usuario/grupo que nginx pueda usar, típicamente www-data.
Task 8: Valida que la configuración de PHP-FPM se parsea correctamente
cr0x@server:~$ sudo php-fpm8.3 -t
[30-Dec-2025 09:24:10] NOTICE: configuration file /etc/php/8.3/fpm/php-fpm.conf test is successful
Qué significa: No hay errores de sintaxis.
Decisión: es seguro reiniciar PHP-FPM después de los cambios; si esto falla, no reinicies en producción hasta arreglarlo.
Task 9: Reinicia PHP-FPM y verifica que cambió la propiedad del socket
cr0x@server:~$ sudo systemctl restart php8.3-fpm
cr0x@server:~$ sudo ls -l /run/php/php8.3-fpm.sock
srw-rw---- 1 www-data www-data 0 Dec 30 09:25 /run/php/php8.3-fpm.sock
Qué significa: El socket ahora pertenece a www-data, modo 660.
Decisión: nginx debería poder conectar. A continuación, valida con una petición y los logs.
Task 10: Confirma que la configuración de nginx es válida y recarga
cr0x@server:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
cr0x@server:~$ sudo systemctl reload nginx
Qué significa: nginx aceptó la config y recargó sin cortar conexiones.
Decisión: si cambiaste alguna ruta de upstream, esto es obligatorio; de lo contrario estás probando la realidad equivocada.
Task 11: Reproduce y verifica una petición FastCGI exitosa
cr0x@server:~$ curl -sS -D- http://127.0.0.1/index.php -o /dev/null | head -n 8
HTTP/1.1 200 OK
Server: nginx
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
Qué significa: nginx llegó a PHP-FPM con éxito.
Decisión: cierra el incidente y luego haz la solución durable (tmpfiles/overrides) para que sobreviva reinicios y boots.
Task 12: Si aún falla, prueba conectar al socket como el usuario de nginx
cr0x@server:~$ sudo -u www-data bash -lc 'php -v >/dev/null; test -S /run/php/php8.3-fpm.sock && echo "socket exists"; cat /run/php/php8.3-fpm.sock'
socket exists
cat: /run/php/php8.3-fpm.sock: No such device or address
Qué significa: El usuario puede ver el socket. El error de cat es normal para sockets; no es un archivo para leer.
Decisión: la visibilidad está bien; si nginx sigue fallando, busca AppArmor o una ruta incorrecta de nginx (o pools múltiples).
Task 13: Busca denegaciones de AppArmor que bloqueen acceso
cr0x@server:~$ sudo journalctl -k --since "30 min ago" | grep -i apparmor | tail -n 5
Dec 30 09:26:01 server kernel: audit: type=1400 audit(1767086761.123:81): apparmor="DENIED" operation="connect" profile="/usr/sbin/nginx" name="/run/php/php8.3-fpm.sock" pid=1842 comm="nginx" requested_mask="wr" denied_mask="wr" fsuid=33 ouid=33
Qué significa: Los permisos están bien, pero la política bloquea a nginx para conectar.
Decisión: ajusta el perfil de AppArmor (o usa rutas por defecto de la distro), no los permisos de sistema de ficheros.
Task 14: Verifica que php-fpm está escuchando en el socket esperado
cr0x@server:~$ sudo ss -xlpn | grep php-fpm
u_str LISTEN 0 4096 /run/php/php8.3-fpm.sock 11159 * 0 users:(("php-fpm8.3",pid=1012,fd=8))
Qué significa: El proceso maestro está escuchando en ese socket.
Decisión: si escucha en otro lado, estás persiguiendo la ruta equivocada en nginx o el pool equivocado.
Task 15: Confirma que systemd no está recreando directorios con modos inesperados
cr0x@server:~$ systemd-tmpfiles --cat-config | grep -nE '/run/php|php-fpm' | head -n 20
219: d /run/php 0755 root root -
Qué significa: tmpfiles define cómo se crea /run/php en el arranque.
Decisión: si esto no coincide con la propiedad/modo deseados, añade un override en /etc/tmpfiles.d/.
La pequeña solución que elimina los 502 (y por qué funciona)
La “pequeña solución” casi siempre está en la configuración del pool de PHP-FPM, no en nginx. Quieres que PHP-FPM cree el socket con
propietario/grupo/modo que coincidan con el worker de nginx. En Debian, nginx típicamente corre como www-data. Ponlo así.
Arregla el pool: establece listen.owner, listen.group, listen.mode
Edita el archivo del pool (a menudo /etc/php/8.3/fpm/pool.d/www.conf, pero tu pool puede llamarse diferente).
Estas son las líneas que importan:
cr0x@server:~$ sudo sed -n '35,55p' /etc/php/8.3/fpm/pool.d/www.conf
listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
Por qué funciona: PHP-FPM crea el socket. Si lo crea como root:root, nginx no podrá conectar.
Si lo crea como www-data:www-data con 0660, nginx conecta limpiamente y asunto resuelto.
Cuando no usar www-data
Si ejecutas múltiples sitios y quieres aislamiento, no metas todo en www-data. Usa un grupo dedicado
(por ejemplo, nginx-php), deja nginx como www-data y añade www-data a ese grupo,
mientras los pools de PHP-FPM establecen listen.group = nginx-php. Eso te da un límite compartido controlado.
cr0x@server:~$ sudo groupadd --system nginx-php
cr0x@server:~$ sudo usermod -aG nginx-php www-data
cr0x@server:~$ id www-data
uid=33(www-data) gid=33(www-data) groups=33(www-data),990(nginx-php)
Decisión: Si necesitas separación por sitio, elige una estrategia de grupo compartido. Si es una caja de app única, www-data por todas partes está bien.
Haz que la solución perdure: no hagas chmod al socket directamente
Si haces esto:
cr0x@server:~$ sudo chown www-data:www-data /run/php/php8.3-fpm.sock
cr0x@server:~$ sudo chmod 660 /run/php/php8.3-fpm.sock
Puede que inmediatamente dejes de ver los 502. También se evaporará en el siguiente reinicio. Eso no es una solución de ingeniería; es un ritual de alivio.
Broma #2: Hacer chmod a un socket en tiempo de ejecución a mano es como reparar un tejado con una nota adhesiva—reconfortante, breve y fundamentalmente irrespetuoso con la física.
systemd-tmpfiles: la pieza que falta para sockets bajo /run
Aquí está la sutileza que pica a la gente: incluso si PHP-FPM crea el socket correctamente, puede fallar al crearlo si el directorio
no existe o no es traversable. Y /run es efímero. Necesitas que el directorio /run/php se cree al arrancar,
con una propiedad y modo sensatos. Debian normalmente hace esto por ti, pero endurecimientos personalizados o trabajos de limpieza pueden romperlo.
Comprueba el estado del directorio /run/php
cr0x@server:~$ sudo ls -ld /run/php
drwxr-x--- 2 root www-data 80 Dec 30 09:25 /run/php
Qué significa: El grupo es www-data y está presente el bit de ejecución. nginx puede traversar; bien.
Decisión: si esto fuera drwx------ root root, nginx fallaría incluso si el socket fuera perfecto.
Crea una regla tmpfiles (cuando los valores por defecto no bastan)
Si tienes un grupo no estándar (como nginx-php) o cambiaste las convenciones de propiedad, crea tu propia entrada tmpfiles.
Esto evita depender de defaults de paquete que pueden no coincidir con tu entorno.
cr0x@server:~$ printf '%s\n' 'd /run/php 0750 root nginx-php -' | sudo tee /etc/tmpfiles.d/php-sockets.conf
d /run/php 0750 root nginx-php -
Aplícalo de inmediato:
cr0x@server:~$ sudo systemd-tmpfiles --create /etc/tmpfiles.d/php-sockets.conf
cr0x@server:~$ sudo ls -ld /run/php
drwxr-x--- 2 root nginx-php 80 Dec 30 09:28 /run/php
Decisión: si administras flotas, esto es la diferencia entre “funciona en mi máquina” y “sigue funcionando después del reboot.”
No olvides reiniciar servicios que hayan cacheado membresía de grupo
Cuando añades www-data a un grupo nuevo, procesos ya en ejecución pueden no captarlo. nginx recarga workers, pero algunos gestores
de servicios hacen cosas extrañas según tu configuración. Verifica con ps y logs, o reinicia nginx si hace falta.
cr0x@server:~$ sudo systemctl restart nginx
cr0x@server:~$ ps -o pid,user,group,comm -C nginx
PID USER GROUP COMMAND
2101 root root nginx
2102 www-data www-data nginx
2103 www-data www-data nginx
Qué significa: El grupo primario sigue mostrando www-data (eso está bien). Los grupos suplementarios no se muestran aquí.
Decisión: usa /proc status para confirmar grupos suplementarios si es necesario.
cr0x@server:~$ sudo awk '/Groups:/{print}' /proc/2102/status
Groups: 33 990
Qué significa: El worker tiene tanto www-data como nginx-php.
Decisión: el acceso basado en grupo al socket funcionará ahora de forma fiable.
Configuración de upstream en nginx: no te sabotees
Errores de configuración de nginx pueden disfrazarse de errores de permisos de socket. O combinarse con ellos para un fallo doble.
Trata a nginx como un instrumento preciso, no como un archivo de texto al que pinchas hasta que deja de chillar.
Usa la sintaxis correcta de fastcgi_pass
En Debian, verás comúnmente:
cr0x@server:~$ sudo awk 'NR>=1 && NR<=60 {print NR ":" $0}' /etc/nginx/sites-enabled/app.conf | sed -n '15,40p'
15:location ~ \.php$ {
16: include snippets/fastcgi-php.conf;
17: fastcgi_pass unix:/run/php/php8.3-fpm.sock;
18:}
Decisión: mantenlo aburrido. No inventes un include personalizado a menos que tengas una necesidad específica y cobertura de pruebas.
Cuidado con rutas obsoletas tras actualizaciones de PHP
Las actualizaciones en Debian 13 suelen coincidir con saltos de versión de PHP. Si actualizaste desde una versión anterior, puede que tengas un sitio nginx
apuntando aún a /run/php/php8.2-fpm.sock mientras PHP 8.3 está instalado. Eso produce “No such file.”
Tu arreglo entonces no es de permisos; es de precisión.
cr0x@server:~$ sudo ls -1 /run/php
php8.3-fpm.sock
Decisión: si solo existe un socket, alinea nginx a ese socket o crea un pool/socket que coincida con la ruta prevista.
Pools múltiples: nómbralos y apunta nginx explícitamente
Si tienes múltiples aplicaciones, no señales todo a “www” por costumbre.
Crea archivos de pool separados y sockets separados. Luego usa un bloque upstream por app si eso mejora la legibilidad.
Registro que merece su espacio en disco
Los problemas de permisos de socket son fáciles de detectar si tienes el nivel de logs correcto. Si no lo tienes, son un costoso juego de adivinanzas.
Los errores de nginx suelen ser suficientes, pero los logs de PHP-FPM pueden confirmar comportamiento a nivel de pool (especialmente en setups con múltiples pools).
Revisa logs de PHP-FPM alrededor de arranque/reinicio
cr0x@server:~$ sudo journalctl -u php8.3-fpm --since "30 min ago" --no-pager | tail -n 30
Dec 30 09:25:03 server php-fpm8.3[1012]: NOTICE: fpm is running, pid 1012
Dec 30 09:25:03 server php-fpm8.3[1012]: NOTICE: ready to handle connections
Decisión: si ves fallos de bind/listen aquí, arregla la configuración del pool de PHP-FPM o la creación del directorio. Si los logs están limpios, mantén el foco en el acceso nginx→socket.
Activa detalle útil en logs de nginx durante respuesta a incidentes
Temporalmente sube el nivel de logging de nginx si estás atascado. Luego bájalo. El logging es como la cafeína: útil en una emergencia, malo como estilo de vida.
Tres micro-historias corporativas desde las trincheras
1) Incidente causado por una suposición errónea: “root lo posee, así que debe ser seguro”
Una compañía mediana migró un monolito PHP desde una VM antigua a Debian 13. Los ingenieros hicieron las cosas bien—imágenes inmutables,
pipeline de deploy, config en Git. También heredaron un snippet de “hardening” que establecía
listen.owner = root y listen.group = root “para prevenir acceso”.
La suposición era que nginx, siendo “el servidor web”, debía ser lo bastante privilegiado para conectar de todos modos. En sus cabezas,
nginx era un proceso con autoridad. En realidad, nginx tiene un master privilegiado y workers sin privilegios,
y son los workers los que necesitan conectar a upstreams. Los workers eran www-data.
El corte inicial parecía bien porque el entorno antiguo usaba TCP, no sockets. El entorno nuevo usó sockets Unix por “rendimiento”.
Inmediatamente después del corte, aparecieron 502—solo en rutas PHP. Los archivos estáticos estaban bien, lo que hizo que el incidente pareciera una regresión de PHP.
Un desarrollador revertió código. Sin cambio. Alguien reinició php-fpm. Sin cambio. Alguien reinició nginx. Alivio breve, luego falla de nuevo.
El punto de inflexión fue una línea en los logs de nginx: errno 13. Una vez que vieron que el socket era root:root 0660, se acabó.
Cambiaron listen.group a un grupo compartido, reiniciaron PHP-FPM y el tráfico se estabilizó. La lección del postmortem fue directa:
los modelos de permisos no son “vibras”. Si no sabes qué proceso necesita acceso, configuras según superstición.
2) Optimización que salió mal: “Los sockets Unix son más rápidos, así que activemos todo”
Una organización grande tenía un equipo de plataforma estandarizando pilas web. Ejecutaban muchos pares nginx+PHP-FPM.
Alguien notó que los sockets Unix evitan overhead TCP y decidió estandarizar en sockets en toda la flota.
El cambio se desplegó como una “optimización segura” detrás de un feature flag.
Funcionó en staging. Por supuesto que sí. Staging era un host único con un pool único, permisos vanilla,
y nadie había tocado la configuración de tmpfiles desde tiempos inmemoriales.
Producción era un lío. Algunos hosts corrían múltiples pools con usuarios por app. Algunos tenían nginx en contenedor.
Algunos tenían perfiles personalizados de AppArmor. Algunos tenían jobs de limpieza que eliminaban directorios de tiempo de ejecución durante “mantenimiento”
porque alguien confundió /run con “cache”.
El despliegue no causó una caída limpia. Causó una hemorragia lenta: un pequeño porcentaje de peticiones fallaba con 502,
correlacionado con nodos específicos. Eso es peor. Una caída limpia atrae atención; la falla parcial se culpa a “la app”.
La solución no fue “volver a TCP”. Fue tratar los sockets como infraestructura: definir la creación de directorios explícitamente,
definir la propiedad del socket vía config del pool y documentar la estrategia de grupos. La optimización dejó de fallar cuando dejó de ser “solo un toggle”.
3) Práctica aburrida pero correcta que salvó el día: “nginx -T y namei antes de entrar en pánico”
Un equipo de servicios financieros seguía un proceso de cambios estricto y se burlaban de ellos por ello. Pero sus rotaciones on-call eran más tranquilas.
Durante un rollout a Debian 13, un nodo empezó a devolver 502 en rutas PHP. El on-call no reinició nada al principio.
Siguió un runbook corto: captura el error de nginx, imprime la configuración activa de nginx, inspecciona socket y directorio con namei.
En cinco minutos identificaron al culpable: nginx apuntaba a una ruta de socket de un nombre de pool anterior. El nuevo archivo de pool existía,
php-fpm estaba corriendo y el socket correcto existía—simplemente no en la ruta a la que nginx estaba configurado. No era permisos.
Era un desajuste de configuración.
Actualizaron el archivo del sitio nginx, ejecutaron nginx -t, recargaron nginx y los 502 se detuvieron. No fue necesario reiniciar PHP-FPM.
Eso importa en entornos donde reiniciar php-fpm puede cortar peticiones en vuelo o disparar calentamientos lentos.
La práctica que salvó el día fue aburrida: siempre verifica qué está realmente corriendo (nginx -T) y siempre verifica
la traversión del sistema de ficheros (namei -l). Lo aburrido es fiable. Lo fiable es rentable.
Errores comunes: síntoma → causa raíz → solución
1) Síntoma: nginx muestra “Permission denied (13)” sobre el socket
Causa raíz: Socket propiedad del usuario/grupo equivocado, o modo demasiado restrictivo.
Solución: Establece en el pool de PHP-FPM:
listen.owner, listen.group, listen.mode = 0660. Reinicia PHP-FPM y verifica con ls -l.
2) Síntoma: “No such file or directory (2)” para la ruta del socket
Causa raíz: nginx apunta a la ruta de socket equivocada (salto de versión PHP, pool renombrado), o PHP-FPM no creó el socket.
Solución: Verifica la config activa de nginx con nginx -T. Verifica el listen = del pool de PHP-FPM. Comprueba que el socket exista en /run/php.
3) Síntoma: Funciona después de chmod, falla tras reinicio
Causa raíz: El cambio manual de permisos se pierde cuando PHP-FPM recrea el socket o cuando /run se resetea.
Solución: Cambia la configuración del pool (permisos en momento de creación) y asegúrate de que /run/php se cree vía tmpfiles si es necesario.
4) Síntoma: Los permisos parecen correctos, sigue “Permission denied”
Causa raíz: La política de AppArmor bloquea a nginx conectar a esa ruta de socket.
Solución: Confirma con logs de auditoría del kernel. Ajusta el perfil de AppArmor o vuelve a rutas estándar cubiertas por perfiles existentes.
5) Síntoma: 502s aleatorios durante despliegues, especialmente con recargas
Causa raíz: Cambio de ruta de socket entre versiones/pools, o múltiples pools reiniciados en orden incorrecto, dejando a nginx apuntando a un socket temporalmente ausente.
Solución: Estabiliza la ruta del socket, coordina reinicios y evita cambiar nombres de socket como parte de despliegues rutinarios.
6) Síntoma: Solo fallan algunos vhosts; otros están bien
Causa raíz: Un sitio apunta al pool/socket equivocado; otros sitios están correctos.
Solución: Audita cada valor fastcgi_pass de los vhosts; no asumas consistencia. Usa sockets de pool explícitos por sitio.
7) Síntoma: PHP-FPM está activo, pero el socket nunca aparece
Causa raíz: Misconfiguración del pool, directorio faltante o fallo de permisos al crear el directorio.
Solución: Revisa journalctl -u php8.3-fpm. Asegura que /run/php exista con modo/propiedad correctos. Valida la config con php-fpm8.3 -t.
Listas de verificación / plan paso a paso
Checklist de respuesta a incidentes (15 minutos, host único)
- Captura la línea de error de nginx para una petición fallida desde
/var/log/nginx/error.log. - Confirma la ruta del socket upstream desde la config activa usando
nginx -T. - Comprueba la salud del servicio PHP-FPM con
systemctl status php8.3-fpm. - Inspecciona el socket con
ls -ly la traversión del path connamei -l. - Confirma el usuario de los workers de nginx con
ps. - Compara la config del pool:
listen,listen.owner,listen.group,listen.mode. - Valida la config:
php-fpm8.3 -t. - Reinicia PHP-FPM (solo tras validar config), luego vuelve a comprobar propiedad/modo del socket.
- Recarga nginx (tras
nginx -t) y prueba concurl. - Si sigue bloqueado, revisa denegaciones de AppArmor vía logs de auditoría del kernel.
Checklist de hardening (hacerlo duradero tras reboots)
- Decide un modelo de acceso al socket: usuario único (
www-data) o grupo compartido (recomendado para pools multiusuario). - Asegura que la propiedad/modo de
/run/phpcoincidan con el modelo. - Si te desvías de los valores por defecto de la distro, añade una regla tmpfiles bajo
/etc/tmpfiles.d/. - Mantén rutas de socket estables; no codifiques versiones menores de PHP en cientos de vhosts sin automatización.
- Documenta el mapeo pool→vhost: qué socket usa cada bloque server.
- Incluye muestreo de logs en tu validación: grep por
Permission deniedtras cambios.
Plan de cambio (despliegue seguro para una flota)
- Inventaría todos los fastcgi_pass en las configs de nginx.
- Inventaría todos los pools de PHP-FPM y sus ajustes de listen.
- Elige un estándar: socket por sitio con grupo compartido, o un pool compartido.
- Prueba el cambio por etapas: ajusta configs de pool primero, luego nginx, luego recarga/reinicia en orden controlado.
- Automatiza la verificación: comprueba que el socket existe, propiedad, modo y que nginx puede servir un endpoint de salud PHP.
- Despliega con canarios. No “lo enciendas todo” salvo que disfrutes descubrir unknown unknowns a las 2 a.m.
Preguntas frecuentes (FAQ)
1) ¿Por qué nginx muestra 502 en vez de un error de permisos más claro?
nginx es un reverse proxy. Cuando no puede hablar con el upstream, devuelve “Bad Gateway.” La razón detallada vive en el log de errores de nginx,
no en la respuesta HTTP.
2) ¿Debería usar TCP en vez de sockets Unix para evitar esto?
TCP evita permisos de sistema de ficheros y reglas de AppArmor por ruta, pero introduce gestión de puertos y potencialmente mayor exposición.
Los sockets Unix están bien—solo configura propiedad/modo correctamente y mantén la ruta estable.
3) ¿Es aceptable hacer chmod 777 al socket alguna vez?
No. “Funciona” eliminando el control de acceso necesario. También enseña a tu equipo a resolver incidentes ampliando el radio de impacto.
Usa propietario/grupo/modo correctos o un grupo compartido dedicado.
4) ¿Cuál es el modo de permisos más seguro para un socket al que nginx se conecta?
Típicamente 0660 con propiedad php-fpm-user:shared-group, y nginx en ese grupo compartido.
Evita acceso global. Evita root:root a menos que nginx también sea privilegiado (no debería).
5) ¿Por qué no persisten los cambios de permisos en el archivo socket?
PHP-FPM crea el socket cuando el pool arranca. Al reiniciar, borra y recrea el socket aplicando la configuración del pool.
Además, /run es tmpfs y se resetea al boot.
6) Puse listen.owner y listen.group, pero sigue creando sockets propiedad de root. ¿Por qué?
Puedes estar editando el archivo de pool equivocado, o el servicio usa un directorio de configuración diferente, o tienes múltiples pools creando distintos sockets.
Confirma con grep en /etc/php/8.3/fpm/pool.d y verifica la ruta del socket que nginx usa.
7) ¿Los permisos de directorio pueden romper esto aunque el socket parezca correcto?
Sí. nginx debe poder traversar cada directorio padre para alcanzar el socket. Usa namei -l para ver dónde falla la traversión.
8) ¿Cómo aparece AppArmor distinto a un problema de permisos de sistema de ficheros?
Los permisos del sistema de ficheros normalmente producen logs de error de nginx con errno 13 y sin línea de auditoría del kernel.
AppArmor suele emitir una entrada de auditoría del kernel “DENIED” nombrando /usr/sbin/nginx y la ruta del socket.
9) Ejecuto pools PHP-FPM como usuarios por app. ¿Cuál es la estrategia de sockets más limpia?
Dale a cada pool su propia ruta de socket y establece listen.group a un grupo compartido al que pertenezca nginx.
Mantén listen.mode = 0660. Esto mantiene el acceso controlado sin caer en el “todo es www-data”.
10) ¿Cómo evito que futuras actualizaciones rompan rutas de socket?
No codifiques versiones menores de PHP en docenas de vhosts sin automatización. Usa nombres de socket estables (por pool),
o gestiona las configs de nginx mediante un sistema que las actualice durante la actualización.
Conclusión: siguientes pasos que puedes hacer hoy
Debian 13 no inventó los problemas de sockets de PHP-FPM. Solo los hizo más fáciles de encontrar: nuevas instalaciones, upgrades,
mayor integración con systemd y el habitual “este servidor es especial” que se acumula silenciosamente hasta que deja de funcionar.
Si no te llevas otra cosa: deja de hacer chmod a sockets en tiempo de ejecución. Arregla la creación del socket en la fuente—el pool de PHP-FPM—y asegúrate de que
/run/php exista con permisos predecibles tras reinicios. Luego verifica con logs y una petición limpia.
Pasos prácticos:
- Ejecuta
nginx -Ty registra la ruta activa del socket para cada vhost. - Inspecciona la propiedad/modo del socket con
ls -ly la traversión connamei -l. - Establece
listen.owner,listen.group,listen.modeen el archivo de pool correcto. - Si usas grupos o rutas personalizadas, añade una regla tmpfiles para que
/run/phpsea determinista. - Vuelve a probar con
curly observa los logs de nginx en silencio: el mejor tipo de silencio.