Actualización de WordPress falló: arregla permisos, espacio en disco y propiedad correctamente

¿Te fue útil?

Las actualizaciones de WordPress deberían ser aburridas. Haces clic en “Actualizar”, esperas unos segundos y sigues con tu vida.
Entonces producción decide recordarte que funciona sobre sistemas de ficheros, permisos y aquella encantadora decisión que tomaste hace seis meses a las 2 a.m.

Si ves “Update failed”, “Could not create directory”, “Permission denied”, “PCLZIP_ERR”, “Disk full” o un sitio atrapado en modo mantenimiento,
este es el camino práctico para salir. No el camino de “chmod 777 hasta que funcione”. El correcto.

Guía rápida de diagnóstico

Cuando fallan las actualizaciones, no empieces cambiando permisos. Empieza identificando el cuello de botella.
WordPress te dice “update failed”, pero el sistema te está diciendo por qué. Tu trabajo es escuchar al sistema.

Primero: determina qué tipo de fallo es

  • ¿Es el sistema de ficheros escribible para el usuario web/PHP? Si no, obtendrás “Permission denied” o “Could not create directory”.
  • ¿Se agotó el espacio en disco o los inodos? Las actualizaciones escriben ficheros temporales, descomprimen archivos y hacen renombres atómicos. Necesitan margen.
  • ¿El directorio temporal es escribible y suficientemente grande? PHP usa /tmp (a menudo tmpfs) o una ruta temporal configurada.
  • ¿WordPress está atascado en modo mantenimiento? Puede haber tenido éxito a medias y dejado .maintenance atrás.
  • ¿El proceso de actualización está agotando tiempo o memoria? Plugins/temas grandes pueden disparar timeouts de PHP y límites de memoria.

Segundo: revisa el error en la capa correcta

  • UI/Registros de WordPress: Los mensajes del admin suelen ser vagos. Mejor que nada, pero no suficiente.
  • Registros del servidor web / PHP-FPM: A menudo contienen la ruta exacta con “Permission denied” o “No space left on device”.
  • Capa del sistema: df, mount, ls -l, namei, getfacl. Aquí vive la verdad.

Tercero: aplica la solución de menor riesgo primero

  1. Espacio en disco/inodos: Es seguro comprobarlo y a menudo explica inmediatamente fallos “misteriosos”.
  2. Desajustes de propiedad: Corrige la discrepancia para que el usuario previsto pueda escribir. Evita la escritura global.
  3. Bits de permiso/ACL: Ajusta al mínimo necesario. Mantén wp-config.php protegido.
  4. Timeout/memoria: Si falla a mitad de descompresión o descarga, ajusta límites de PHP o usa WP-CLI.

Un principio guía: si tu arreglo es “hacer todo 777”, no lo arreglaste. Solo trasladaste el radio de explosión hacia la seguridad.

Cómo funcionan realmente las actualizaciones de WordPress (y por qué fallan)

Las actualizaciones de WordPress son operaciones de archivos disfrazadas de botón. WordPress (vía PHP) descarga un zip, lo escribe en un área temporal,
lo descomprime y luego reemplaza archivos en su lugar. Los plugins y temas son similares, solo apuntan a directorios diferentes.

Las rutas comunes de actualización

  • Actualizaciones del core: escriben en la raíz de WordPress y en wp-includes/wp-admin. También escriben .maintenance.
  • Actualizaciones de plugins: escriben en wp-content/plugins y pueden reemplazar un directorio de plugin entero.
  • Actualizaciones de temas: escriben en wp-content/themes.
  • Paquetes de idioma: escriben en wp-content/languages.

Por qué ocurre el fallo

Las actualizaciones fallan cuando PHP no puede escribir donde necesita, o no puede crear archivos temporales, o no puede renombrar directorios,
o se queda sin recursos a mitad de proceso. El mensaje de error que ves depende de qué función falló: inicialización del sistema de ficheros, unzip, copy, rename o cleanup.

El mayor error conceptual: pensar que “WordPress” posee estos ficheros. No es así. El SO los posee. El usuario del servidor web los posee. Tu proceso de despliegue los controla.
WordPress es solo un proceso PHP que pide al SO permiso para realizar operaciones en el sistema de ficheros.

Datos e historia interesantes (corto, útil, algo nerd)

  1. Las actualizaciones automáticas en segundo plano del core llegaron a hacerse comunes alrededor de la versión 3.7, cambiando los modos de fallo de “el humano se olvidó” a “la máquina intentó y chocó con permisos”.
  2. WordPress usa una capa de abstracción de sistema de ficheros que puede elegir entre escrituras “directas” y métodos FTP/SSH según lo que detecte.
  3. Muchos problemas de “permisos de WordPress” son en realidad problemas de recorrido de directorio padre; un bit no ejecutable (x) en un directorio puede bloquear todo lo que está debajo.
  4. Los inodos importan tanto como los bytes; puedes tener gigabytes libres y aun así fallar si consumiste todos los inodos (común en sistemas de ficheros con muchos archivos pequeños).
  5. Los renombres atómicos son un truco clásico de despliegue, pero requieren permiso de escritura en el directorio padre, no solo en el objetivo.
  6. tmpfs para /tmp es común en Linux moderno; es rápido, pero se dimensiona desde RAM y puede llenarse durante operaciones de unzip.
  7. La extracción zip en PHP históricamente dependió de herramientas externas o librerías; la falta de soporte ZipArchive aún aparece como errores de unzip en algunas configuraciones.
  8. “WordPress atascado en modo mantenimiento” suele ser un archivo sobrante llamado .maintenance, creado durante las actualizaciones y eliminado tras el éxito.

Tareas prácticas: comandos, salidas, decisiones (12+)

Estas son las comprobaciones que ejecuto en producción, en el orden en que más rinden. Cada tarea incluye:
el comando, lo que significa una salida típica y la decisión que tomas en función de ello.

Tarea 1: Confirmar espacio en disco en el montaje relevante

cr0x@server:~$ df -hT /var/www/html
Filesystem     Type  Size  Used Avail Use% Mounted on
/dev/vda1      ext4   40G   39G  520M  99% /

Significado: 99% usado. Las actualizaciones necesitan espacio para descargas, extracción y a veces doble escritura.

Decisión: Arregla el espacio en disco antes de tocar permisos. Limpia logs, backups antiguos, cachés o expande el sistema de ficheros. Si haces “chmod” ahora, solo estás puliendo un barco que se hunde.

Tarea 2: Comprobar agotamiento de inodos (sí, sucede)

cr0x@server:~$ df -i /var/www/html
Filesystem     Inodes  IUsed   IFree IUse% Mounted on
/dev/vda1     2621440 2621400     40  100% /

Significado: Te quedaste sin inodos. Crear un archivo más falla con “No space left on device” aunque bytes estén disponibles.

Decisión: Identifica directorios con muchos archivos (carpetas de caché, dirs de sesión) y purga. No reinicies esperando que eso borre archivos; no lo hace.

Tarea 3: Identificar el usuario de ejecución de PHP (el que realmente escribe)

cr0x@server:~$ ps -eo user,comm,args | egrep 'php-fpm|apache2|httpd' | head
www-data php-fpm8.2 php-fpm: pool www
www-data php-fpm8.2 php-fpm: pool www
root     nginx      nginx: master process /usr/sbin/nginx -g daemon on;

Significado: Los workers de PHP-FPM se ejecutan como www-data. Nginx master es root, pero los workers pasan a PHP-FPM.

Decisión: Los permisos de escritura deben funcionar para www-data, no para tu usuario SSH. Si arreglas la propiedad para ti, las actualizaciones seguirán fallando.

Tarea 4: Validar rápidamente la propiedad de la raíz de WordPress y wp-content

cr0x@server:~$ ls -ld /var/www/html /var/www/html/wp-content
drwxr-xr-x  5 root     root     4096 Dec 27 08:10 /var/www/html
drwxr-xr-x 10 root     root     4096 Dec 27 08:10 /var/www/html/wp-content

Significado: Root posee todo; el grupo no es escribible; el usuario web no puede escribir. Las actualizaciones de plugins/temas fallarán.

Decisión: Decide tu modelo de propiedad (ver más abajo). Para la mayoría de instalaciones monousuario: haz que www-data sea propietario de las rutas escribibles, o establece propiedad de grupo y usa un grupo compartido.

Tarea 5: Confirmar la ruta exacta que falla desde los logs

cr0x@server:~$ sudo tail -n 50 /var/log/php8.2-fpm.log
[27-Dec-2025 08:12:19] WARNING: [pool www] child 22190 said into stderr: "PHP Warning:  copy(/var/www/html/wp-content/plugins/akismet/.htaccess): failed to open stream: Permission denied in /var/www/html/wp-admin/includes/class-wp-filesystem-direct.php on line 309"

Significado: Esto no es un vago “update failed.” Es una ruta de archivo y función específicas. El SO denegó la escritura.

Decisión: Corrige permisos/propiedad para ese subárbol de directorios. Si la ruta está bajo wp-content, ese es tu zona escribible.

Tarea 6: Verificar permisos de recorrido de directorio con namei

cr0x@server:~$ namei -l /var/www/html/wp-content/plugins
f: /var/www/html/wp-content/plugins
drwxr-xr-x root root /
drwxr-xr-x root root var
drwxr-xr-x root root www
drwxr-xr-x root root html
drwxr-xr-x root root wp-content
drwxr-xr-x root root plugins

Significado: Incluso si plugins parecía correcto, debes tener execute (x) en cada directorio del camino para poder recorrerlo.

Decisión: Si algún directorio padre carece de x para el usuario web (o grupo), corrígelo. “Pero wp-content es escribible” no ayuda si /var/www bloquea el recorrido.

Tarea 7: Comprobar ACLs si los permisos parecen “correctos” pero sigue fallando

cr0x@server:~$ getfacl -p /var/www/html/wp-content | sed -n '1,20p'
# file: /var/www/html/wp-content
# owner: root
# group: root
user::rwx
group::r-x
other::r-x

Significado: No hay entradas ACL aquí; son los bits de modo clásicos. Si ves una ACL que niega acceso, eso puede anular tus expectativas.

Decisión: Si existen ACL y están mal, arréglalas intencionalmente. No elimines ACLs al azar a menos que sepas por qué se añadieron.

Tarea 8: Confirmar que el montaje no está en solo lectura

cr0x@server:~$ mount | grep ' / '
/dev/vda1 on / type ext4 (rw,relatime,errors=remount-ro)

Significado: Montado en lectura-escritura ahora, pero errors=remount-ro significa que un error de sistema de ficheros podría cambiarlo a solo lectura.

Decisión: Si ves (ro,...), para. Arregla el problema de disco/sistema de ficheros subyacente primero (dmesg, fsck, salud del almacenamiento). Las actualizaciones no pueden continuar.

Tarea 9: Verificar directorio temporal de PHP y su espacio libre

cr0x@server:~$ php -i | egrep 'upload_tmp_dir|sys_temp_dir|temporary' | head
upload_tmp_dir => no value => no value
sys_temp_dir => no value => no value
cr0x@server:~$ df -hT /tmp
Filesystem Type  Size  Used Avail Use% Mounted on
tmpfs      tmpfs 1.0G  980M   44M  96% /tmp

Significado: PHP usa el temp del sistema (a menudo /tmp). Si /tmp es pequeño o está lleno, la descompresión/descarga falla.

Decisión: Libera espacio en /tmp o establece un directorio temporal dedicado en disco con permisos correctos para PHP-FPM.

Tarea 10: Confirmar disponibilidad de ZipArchive

cr0x@server:~$ php -m | grep -i zip
zip

Significado: Soporte zip cargado. Si falta, WordPress puede recurrir a métodos más lentos o fallar según el entorno.

Decisión: Si no está presente, instala/activa la extensión PHP zip y reinicia PHP-FPM. Los errores de unzip suelen ser “dependencia faltante”, no “WordPress roto”.

Tarea 11: Buscar el archivo de modo mantenimiento

cr0x@server:~$ ls -la /var/www/html/.maintenance
-rw-r--r-- 1 root root 58 Dec 27 08:12 /var/www/html/.maintenance

Significado: WordPress piensa que una actualización está en curso. Si una actualización se bloqueó, esto puede persistir y bloquear el sitio.

Decisión: Elimínalo solo después de confirmar que no hay una actualización activa. Luego vuelve a ejecutar la actualización.

Tarea 12: Usar WP-CLI para reproducir el fallo con salida más clara

cr0x@server:~$ cd /var/www/html
cr0x@server:~$ sudo -u www-data wp plugin update --all
Downloading update from https://downloads.wordpress.org/plugin/example.1.2.3.zip...
Unpacking the update...
Error: Could not create directory '/var/www/html/wp-content/upgrade'.

Significado: El fallo es consistente y apunta al directorio de trabajo de upgrade.

Decisión: Asegúrate de que wp-content sea escribible y permite concretamente crear wp-content/upgrade.

Tarea 13: Verificar propiedad y permisos en el directorio que falla

cr0x@server:~$ ls -ld /var/www/html/wp-content /var/www/html/wp-content/upgrade
drwxr-xr-x 10 root root 4096 Dec 27 08:10 /var/www/html/wp-content
ls: cannot access '/var/www/html/wp-content/upgrade': No such file or directory

Significado: No puede crear upgrade porque wp-content no es escribible por PHP.

Decisión: Corrige la propiedad/grupo y escritura en wp-content (no necesariamente en toda la raíz de WordPress).

Tarea 14: Detectar “permission denied” causado por atributo inmutable

cr0x@server:~$ lsattr -d /var/www/html/wp-content
-------------e---- /var/www/html/wp-content

Significado: No hay bandera inmutable aquí. Si ves i, el fichero/directorio es inmutable y no se puede modificar ni siquiera como root.

Decisión: Si está establecido inmutable, quítalo deliberadamente en las rutas afectadas. Normalmente esto fue un intento de endurecimiento o un script de backup mal orientado.

Arreglar permisos y propiedad sin romper la seguridad

Necesitas un modelo. “Solo hacerlo escribible” no es un modelo. Un modelo te dice quién puede escribir qué directorios y por qué.

Los tres modelos de propiedad sensatos

Modelo A: El usuario web posee el contenido escribible (común, simple, más arriesgado)

En este modelo, www-data (o el usuario bajo el que corre PHP) posee wp-content y puede escribir subidas y actualizaciones de plugins/temas.
Los ficheros del core pueden permanecer propiedad de root para reducir el riesgo de manipulación del core.

Úsalo cuando: servidor único, equipo pequeño, se esperan actualizaciones vía UI de admin, aceptas el trade-off de seguridad.

Evítalo cuando: restricciones de cumplimiento, hosting compartido con múltiples sitios, o quieras inmutabilidad para el código.

Modelo B: Propiedad de grupo compartido + directorios setgid (mi predeterminado para equipos)

Crea un grupo (por ejemplo wp), añade tanto al usuario de despliegue como al usuario web, y establece la propiedad de grupo en las áreas escribibles.
Luego usa setgid en directorios para que los nuevos ficheros hereden el grupo.

Úsalo cuando: despliegas vía SSH/CI y aún quieres que WordPress escriba en wp-content.

Modelo C: El usuario web no escribe nada; despliegues y actualizaciones solo por CI (más seguro, más disciplinado)

Las actualizaciones de WordPress ocurren mediante pipelines CI o ventanas de mantenimiento. El usuario web no puede escribir en el código.
Las subidas (media) pueden residir en un montaje escribible separado o integrarse con un store de objetos.

Úsalo cuando: entornos regulados, mucho tráfico, múltiples instancias o te han quemado antes.

Elige uno. Mezclarlos casualmente es como terminar con un sistema de ficheros que solo funciona cuando estás conectado como “la persona correcta” los martes.

Objetivos prácticos y seguros de permisos

  • Directorios: típicamente 755 (o 775 si dependes de escritura de grupo).
  • Ficheros: típicamente 644 (o 664 con escritura de grupo).
  • wp-config.php: normalmente 640 o 600, dependiendo de la estrategia de grupo.

WordPress necesita escribir en wp-content (plugins/temas/idiomas/subidas) para actualizaciones desde el admin.
No necesita escribir en todo lo demás, y no deberías permitirlo salvo que tengas una razón.

Implementar el Modelo B (grupo compartido) sin drama

cr0x@server:~$ sudo groupadd -f wp
cr0x@server:~$ sudo usermod -aG wp www-data
cr0x@server:~$ sudo usermod -aG wp deploy
cr0x@server:~$ sudo chgrp -R wp /var/www/html/wp-content
cr0x@server:~$ sudo find /var/www/html/wp-content -type d -exec chmod 2775 {} \;
cr0x@server:~$ sudo find /var/www/html/wp-content -type f -exec chmod 664 {} \;

Significado: El grupo es wp. Los directorios tienen setgid (2 en 2775) para que los nuevos ficheros mantengan la propiedad de grupo.

Decisión: Si ves ficheros nuevos con grupo incorrecto más adelante, tu umask o la configuración del servicio te está peleando. Arréglalo a continuación.

Establecer un umask previsible para PHP-FPM (opcional pero a menudo necesario)

Si PHP-FPM crea ficheros con permisos restrictivos, las actualizaciones pueden tener éxito parcial y luego fallar cuando otro proceso intenta modificarlos.
La solución es garantizar permisos de creación consistentes.

cr0x@server:~$ sudo grep -R "umask" -n /etc/php/8.2/fpm/pool.d
/etc/php/8.2/fpm/pool.d/www.conf:404:;php_admin_value[error_log] = /var/log/php8.2-fpm.log

Significado: No hay umask explícito en la configuración del pool (común). Los modos de creación dependen de los valores por defecto.

Decisión: Si necesitas artefactos con escritura de grupo, ajusta el umask a nivel de servicio o impónlo con ACLs (más abajo).

Usar ACLs por defecto para mantener consistente la escritura de grupo (potente, fácil de olvidar)

cr0x@server:~$ sudo setfacl -R -m g:wp:rwx /var/www/html/wp-content
cr0x@server:~$ sudo setfacl -R -d -m g:wp:rwx /var/www/html/wp-content
cr0x@server:~$ getfacl -p /var/www/html/wp-content | sed -n '1,25p'
# file: /var/www/html/wp-content
# owner: root
# group: wp
user::rwx
group::r-x
group:wp:rwx
mask::rwx
other::r-x
default:user::rwx
default:group::r-x
default:group:wp:rwx
default:mask::rwx
default:other::r-x

Significado: Cualquier fichero/directorio nuevo bajo wp-content obtiene grupo wp con rwx (sujeto a la máscara).

Decisión: Si tu entorno maneja ACLs bien, esto evita el problema de “creado como 640, ahora la actualización no puede sobrescribirlo”. Si tus backups o herramientas de sincronización no preservan ACLs, documenta el comportamiento.

Broma #1: Si tu plan de incidentes es “chmod -R 777”, enhorabuena: has inventado un generador distribuido de brechas de seguridad.

Corregir problemas de espacio en disco e inodos (los asesinos silenciosos)

Las actualizaciones de WordPress son sorprendentemente glotonas en espacio. El proceso suele necesitar:
espacio para descargar el zip, espacio para extracción de los archivos descomprimidos y luego los ficheros instalados finales.
En la práctica quieres al menos unos cientos de megabytes libres para actualizaciones pequeñas y más para plugins/temas grandes.

Encuentra rápido qué está usando espacio

cr0x@server:~$ sudo du -xhd1 /var | sort -h
8.0M    /var/cache
120M    /var/log
4.2G    /var/lib
12G     /var/www

Significado: /var/www es grande. Eso puede ser subidas, backups o cachés.

Decisión: Profundiza en el directorio más grande. No borres a ciegas. Si son subidas, considera mover media fuera del disco raíz o podar backups antiguos.

Encuentra directorios con muchos inodos (el problema de “muchos archivos pequeños”)

cr0x@server:~$ sudo find /var/www/html/wp-content -xdev -type f | wc -l
412356

Significado: Cientos de miles de archivos. Los plugins de caché pueden hacer esto. Cada archivo consume un inodo.

Decisión: Identifica directorios de caché y configura la caché para usar menos archivos o un almacenamiento diferente. Si es caché, se puede purgar.

Localiza los “comedores de espacio” comunes de WordPress

cr0x@server:~$ sudo du -sh /var/www/html/wp-content/* | sort -h | tail -n 10
120M    /var/www/html/wp-content/languages
1.3G    /var/www/html/wp-content/plugins
3.8G    /var/www/html/wp-content/uploads
9.1G    /var/www/html/wp-content/cache

Significado: La caché es enorme. A menudo es seguro purgarla durante un incidente (con consecuencias de rendimiento).

Decisión: Limpia la caché para recuperar espacio y luego arregla la causa raíz (configuración de caché, rotación de logs, offloading de subidas, dimensionado de disco).

Limpiar un directorio de caché de forma segura (ejemplo)

cr0x@server:~$ sudo systemctl stop php8.2-fpm
cr0x@server:~$ sudo rm -rf /var/www/html/wp-content/cache/*
cr0x@server:~$ sudo systemctl start php8.2-fpm

Significado: Evitas escrituras concurrentes mientras limpias. No siempre es necesario, pero reduce problemas de borde.

Decisión: Si parar PHP-FPM es muy disruptivo, borra con más cuidado (o limpia vía UI del plugin). Pero en emergencia, el espacio vence a la caché.

Directorios temporales, fallos de unzip y límites de PHP

Los errores de unzip suelen culpar a WordPress porque es la parte que ves. Pero la falla a menudo es:
el almacenamiento temporal está lleno, los permisos son incorrectos, falta ZipArchive, o PHP se queda sin memoria a mitad de extracción.

Confirma dónde PHP escribe archivos temporales

cr0x@server:~$ php -r 'echo sys_get_temp_dir().PHP_EOL;'
/tmp

Significado: PHP usa /tmp.

Decisión: Asegúrate de que /tmp sea escribible por el usuario PHP-FPM y tenga espacio. Si /tmp es tmpfs y se llena con frecuencia, mueve el temp a otro lado.

Establecer un directorio temporal dedicado para actualizaciones de WordPress

Un enfoque pragmático: crea /var/tmp/wp-tmp, hazlo propiedad del usuario PHP, y apunta WordPress a él usando WP_TEMP_DIR.

cr0x@server:~$ sudo install -d -o www-data -g www-data -m 1770 /var/tmp/wp-tmp
cr0x@server:~$ sudo grep -n "WP_TEMP_DIR" /var/www/html/wp-config.php || true
cr0x@server:~$ sudo sh -c "printf '\ndefine(\"WP_TEMP_DIR\", \"/var/tmp/wp-tmp\");\n' >> /var/www/html/wp-config.php"
cr0x@server:~$ sudo tail -n 3 /var/www/html/wp-config.php
define("WP_TEMP_DIR", "/var/tmp/wp-tmp");

Significado: WordPress ahora usa un directorio temporal controlado. 1770 da un comportamiento tipo sticky mediante separación por grupo; ajústalo a tu modelo.

Decisión: Si múltiples pools/usuarios usan el mismo directorio, sepáralos. Un temporal compartido se convierte en un lodazal de permisos.

Comprobar límite de memoria de PHP y tiempo máximo de ejecución (común en plugins grandes)

cr0x@server:~$ php -i | egrep 'memory_limit|max_execution_time' | head -n 2
memory_limit => 128M => 128M
max_execution_time => 30 => 30

Significado: 128MB y 30s están bien para sitios pequeños, pero pueden fallar en actualizaciones grandes, discos lentos o servidores sobrecargados.

Decisión: Si las actualizaciones agotan tiempo, aumenta estos parámetros para operaciones de admin/actualización o ejecuta actualizaciones con WP-CLI en shell controlado (aun usa PHP, pero sufre menos el timeout del navegador).

Apache, Nginx, PHP-FPM: modelos de usuario que lo cambian todo

Los permisos no son “un tema de WordPress”. Son “qué usuario Unix está escribiendo archivos”. Diferentes stacks eligen usuarios distintos.

Apache con mod_php

PHP se ejecuta dentro de procesos worker de Apache. El usuario de Apache (a menudo www-data o apache) es el que escribe ficheros.
Si Apache escribe como www-data pero tus despliegues escriben como deploy con umask 077, tendrás propiedad mixta y fallos futuros.

Nginx + PHP-FPM

Nginx sirve ficheros estáticos y reenvía PHP a PHP-FPM. El usuario que escribe es el del pool PHP-FPM, no Nginx.
Esto es bueno: PHP corre con una identidad más controlada. También significa que “Nginx tiene acceso” es irrelevante si PHP-FPM no lo tiene.

Múltiples sitios en un mismo host

Si ejecutas múltiples sitios WordPress bajo el mismo www-data, has creado un entorno de destino compartido.
Una compromisión en un sitio puede escribir en los directorios de otro sitio si los permisos lo permiten.
Si te tomas la seguridad en serio, usa usuarios/pools por sitio y aísla a nivel de sistema de ficheros.

Una cita operacional (idea parafraseada)

“La esperanza no es una estrategia.” — idea parafraseada comúnmente atribuida al liderazgo en ingeniería y operaciones.

Tres micro-historias corporativas sobre patrones reales

Micro-historia 1: El incidente causado por una suposición errónea

Una compañía mediana ejecutaba WordPress para marketing, pero el sistema vivía en una VM “real” de producción con otros servicios.
Su suposición interna era simple: “Si el sitio está en marcha, las actualizaciones son seguras en cualquier momento.”
Esa suposición es cómo te llaman para incidentes por un sitio.

El equipo de marketing hizo clic en actualizar un plugin durante el horario laboral. La actualización falló, dejando .maintenance.
El sitio empezó a devolver respuesta de mantenimiento. Ventas escaló porque la landing de la campaña no funcionaba.
El primer respondedor hizo lo que muchos hacen bajo presión: reinició servicios. No ayudó. Solo hizo que los logs fueran más difíciles de correlacionar.

La causa raíz real fue presión de disco. No lo suficiente para disparar la monitorización, pero sí para que extraer un zip en /tmp fallara.
/tmp era tmpfs dimensionado desde RAM y ya estaba ocupado por procesos no relacionados.
WordPress puso el sitio en mantenimiento, intentó escribir archivos de upgrade, falló y nunca limpió.

La solución fue aburrida: liberar espacio, mover el temp a disco, eliminar .maintenance y volver a ejecutar la actualización via WP-CLI.
La lección no fue “no actualices en horario laboral” (aunque, tal vez). La lección fue: las actualizaciones son operaciones de almacenamiento; trátalas como despliegues.

Micro-historia 2: La optimización que salió mal

Otra organización quería “mejor rendimiento”. Alguien decidió montar todo el directorio de WordPress en un filesystem de red
para que múltiples servidores de aplicación compartieran el mismo código y subidas. El pitch sonó razonable en la reunión.
El filesystem no asistió a esa reunión.

Las actualizaciones empezaron a fallar de forma intermitente con errores raros de unzip y directorios de plugin incompletos.
A veces la UI del admin decía éxito, pero la siguiente carga de página fallaba con ficheros PHP faltantes.
Los ingenieros persiguieron permisos, versiones de PHP e incluso “quizá WordPress está corrupto”.

El problema real fue semántico y de latencia: el comportamiento del filesystem de red respecto a bloqueos y renombres atómicos no coincidía con lo que el proceso de actualización esperaba.
El proceso de actualización se basa en operaciones de sistema de ficheros fiables y rápidas, especialmente al intercambiar directorios.
Bajo carga, las operaciones se retrasaban, agotaban tiempo o devolvían listados de directorio inconsistentes a distintos nodos.

Lo arreglaron separando responsabilidades: el código se desplegó como artefactos inmutables por nodo, las subidas se almacenaron por separado con un diseño acorde a las fortalezas del almacenamiento.
Las actualizaciones pasaron a CI, no al admin UI. El rendimiento mejoró y el proceso de actualización dejó de jugar a la ruleta.

Micro-historia 3: La práctica aburrida pero correcta que salvó el día

Una gran empresa tenía una práctica que parecía anticuada: cada actualización de WordPress se ejecutaba mediante WP-CLI en ventana de mantenimiento,
precedida por un snapshot del sistema de ficheros y seguida por una verificación rápida de integridad.
Nadie lo publicitaba. Simplemente era “el runbook”.

Un día, una actualización de plugin introdujo un error fatal por una dependencia PHP inesperada.
La UI de administración no era accesible debido al error fatal, pero el sitio seguía sirviendo páginas cacheadas—hasta que la caché expiró.
El incidente pudo haberse convertido en una caída total.

Como las actualizaciones se ejecutaban con WP-CLI y los logs estaban capturados, supieron exactamente qué plugin cambió y cuándo.
Y como el sistema de ficheros había sido snapshotado, el rollback tomó segundos.
Revirtieron el directorio del plugin, restauraron el servicio y luego probaron la actualización en staging con el módulo PHP faltante instalado.

No ocurrió nada heroico. Ese es el punto. Las prácticas aburridas son cómo llegas a ser aburrido en medio de un fallo.

Errores comunes: síntoma → causa raíz → solución

Estos son los patrones que veo repetidamente. El síntoma es lo que notarás. La causa raíz es lo que está realmente roto. La solución es específica.

1) “Could not create directory” durante actualización de plugin/tema

  • Síntoma: La actualización falla; WP-CLI muestra que no puede crear wp-content/upgrade o un directorio de plugin.
  • Causa raíz: wp-content no es escribible por el usuario PHP; o directorios padres carecen del bit execute.
  • Solución: Ajusta propiedad/grupo y permisos solo en wp-content; verifica con namei -l y vuelve a ejecutar la actualización.

2) “Update failed” sin detalles; logs muestran “No space left on device”

  • Síntoma: La UI es ambigua; logs de PHP muestran ENOSPC mientras copian/descomprimen.
  • Causa raíz: Disco lleno o inodos agotados; a veces /tmp lleno en tmpfs.
  • Solución: Comprueba df -hT, df -i y df -h /tmp. Libera espacio o mueve el temp. Luego reintenta.

3) Sitio atascado en modo mantenimiento tras actualización fallida

  • Síntoma: El frontend muestra el mensaje de mantenimiento indefinidamente.
  • Causa raíz: .maintenance quedó detrás debido a crash/timeout/error de permisos.
  • Solución: Verifica que no haya procesos de actualización corriendo; elimina .maintenance; corrige el error subyacente; vuelve a ejecutar la actualización vía WP-CLI.

4) Las actualizaciones funcionan cuando las haces como root por SSH pero fallan en la UI

  • Síntoma: Cambios manuales de ficheros tienen éxito; las actualizaciones desde la UI fallan.
  • Causa raíz: Suposición errónea sobre quién escribe ficheros; PHP se ejecuta como un usuario sin privilegios, no como root.
  • Solución: Alinea los permisos del sistema de ficheros con el usuario PHP (o modelo de grupo compartido). Deja de “arreglar” cosas como root y darlo por hecho.

5) Algunos ficheros son propiedad del usuario deploy y otros de www-data; actualizaciones fallan aleatoriamente

  • Síntoma: La actualización de hoy tiene éxito; la de la próxima semana falla. La propiedad es inconsistente en el árbol.
  • Causa raíz: Vías de actualización mixtas: a veces via UI (www-data), otras via SSH/CI (deploy), con umask incompatibles.
  • Solución: Elige un modelo. Usa grupo compartido + setgid + (opcional) ACLs por defecto. Forza un umask consistente en tu proceso de despliegue.

6) “PCLZIP_ERR” o error de unzip en plugins/temas grandes

  • Síntoma: Errores de extracción, directorios incompletos o timeouts.
  • Causa raíz: Poco espacio temporal, falta ZipArchive, límites de memoria/tiempo o almacenamiento lento.
  • Solución: Asegura que la extensión zip esté presente, asigna espacio temporal y ajusta límites de PHP. Prefiere WP-CLI para actualizaciones controladas.

7) Las actualizaciones fallan solo en un nodo del clúster

  • Síntoma: Un nodo informa ficheros de plugin faltantes o versiones distintas.
  • Causa raíz: Discos locales divergieron; semántica de almacenamiento compartido no compatible; despliegues no coordinados.
  • Solución: Deja de actualizar vía admin UI en entornos en clúster. Usa despliegue por CI y mantén el código inmutable entre nodos.

Broma #2: El proceso de actualización es como un gato: si no controlas dónde escribe, elegirá el lugar menos conveniente disponible.

Listas de verificación / plan paso a paso

Checklist de incidente: recupera el sitio y termina la actualización

  1. Comprueba disco e inodos: df -hT, df -i y df -h /tmp. Libera espacio primero.
  2. Comprueba modo mantenimiento: elimina .maintenance solo tras confirmar que no hay actualización corriendo.
  3. Lee los logs: PHP-FPM y logs del servidor web para la ruta que falla exactamente.
  4. Confirma el usuario PHP: identifica si las escrituras ocurren como www-data, apache o un usuario de pool.
  5. Arregla escribibilidad de wp-content: propiedad/grupo/ACL según el modelo elegido. Verifica con namei -l.
  6. Reintenta via WP-CLI: es más claro, scriptable y menos sujeto a timeouts de navegador.
  7. Valida: carga la página principal, el admin y un par de páginas que usen plugins (formularios, checkout, etc.).

Checklist de endurecimiento: prevenir reincidencias

  1. Elige tu modelo de propiedad: web-owned, grupo compartido o CI-only. Documentalo. Hazlo cumplir.
  2. Separa áreas escribibles: mantiene wp-content/uploads escribible; considera marcar el core como solo lectura.
  3. Establece permisos previsibles: setgid en directorios y/o ACLs por defecto para escritura de grupo.
  4. Dimensiona el almacenamiento temporal: directorio temporal dedicado si /tmp es demasiado pequeño o compartido.
  5. Monitorea lo correcto: bytes de disco, inodos y uso de /tmp. Las alertas deben sonar antes de alcanzar 99%.
  6. Prefiere WP-CLI para actualizaciones en producción: captura logs; ejecuta en ventana de mantenimiento; automatiza pasos de rollback.

Paso a paso: runbook seguro para “arreglar permisos” (modelo grupo compartido)

Este es el plan que suele funcionar en organizaciones reales donde tanto humanos como automatizaciones tocan el sistema de ficheros.

  1. Crea el grupo wp; añade tanto www-data como tu usuario deploy.
  2. Establece la propiedad de grupo en wp-content y setgid en sus directorios.
  3. Establece modos de ficheros y directorios para permitir escritura de grupo, pero no escritura global.
  4. Opcionalmente establece ACLs por defecto para mantener el comportamiento aún con umask variable.
  5. Verifica creando un fichero de prueba como www-data y como deploy, confirma grupo y bits de escritura.
cr0x@server:~$ sudo -u www-data bash -lc 'touch /var/www/html/wp-content/.permtest && ls -l /var/www/html/wp-content/.permtest'
-rw-rw-r-- 1 www-data wp 0 Dec 27 08:40 /var/www/html/wp-content/.permtest
cr0x@server:~$ sudo -u deploy bash -lc 'echo ok >> /var/www/html/wp-content/.permtest && tail -n 1 /var/www/html/wp-content/.permtest'
ok

Significado: Ambas identidades pueden escribir. La propiedad y el grupo son consistentes.

Decisión: Si alguno de los usuarios no puede escribir, corrige membresía de grupo, permisos de directorio o máscaras ACL antes de intentar actualizar de nuevo.

Preguntas frecuentes

1) ¿Debo usar alguna vez chmod -R 777 para arreglar actualizaciones de WordPress?

No. “Funciona” permitiendo que cualquiera escriba cualquier cosa, lo cual es una falla de seguridad disfrazada de progreso.
Corrige propiedad y permisos de grupo en su lugar, idealmente limitado a wp-content.

2) ¿Necesito hacer que toda la raíz de WordPress sea escribible?

Normalmente no. Para actualizaciones UI-driven de plugins/temas, wp-content debe ser escribible.
Las actualizaciones de core pueden requerir escribir en la raíz y en directorios del core; muchos equipos evitan eso realizando actualizaciones de core vía CI/WP-CLI con permisos controlados.

3) ¿Por qué WordPress pide credenciales FTP al actualizar?

WordPress lo hace cuando cree que no puede escribir directamente en el sistema de ficheros. Intenta métodos alternativos de sistema de ficheros.
En servidores Linux modernos, normalmente quieres escrituras directas a wp-content (si permites actualizaciones por UI) y permisos correctos, no FTP.

4) Mi actualización falla con “No such file or directory” durante rename. ¿Qué ocurre?

A menudo es una actualización parcial: la extracción sucedió y luego un rename o cleanup falló por permisos o falta de acceso de escritura en el directorio padre.
Revisa la ruta que falla en los logs, verifica permiso de escritura donde ocurre el rename y vuelve a ejecutar via WP-CLI tras limpiar.

5) ¿Cuál es la forma más segura de manejar actualizaciones en un setup multinodo?

No actualices vía admin UI. Usa un pipeline de despliegue para que cada nodo reciba el mismo artefacto versionado.
Mantén el código inmutable; guarda las subidas en un store compartido dedicado si es necesario.

6) Si wp-content es escribible, ¿por qué siguen fallando las actualizaciones?

Porque la ruta hacia él puede no ser transitables (falta bit x), /tmp puede estar lleno, puedes haberte quedado sin inodos, las ACLs pueden negar acceso,
o PHP puede ejecutarse como un usuario distinto al que crees. Usa namei -l, df -i y revisa logs.

7) ¿Está bien que www-data posea directorios de plugins?

Es común y funcional. También es un trade-off de seguridad: si WordPress o un plugin es comprometido, los directorios de plugins escribibles son un mecanismo de persistencia conveniente.
Si puedes, prefiere el modelo de grupo compartido o actualizaciones por CI-only en producción.

8) ¿Cómo arreglo “atrapado en modo mantenimiento” de forma segura?

Confirma que no hay actualización en curso (comprueba procesos y logs recientes), luego borra el archivo .maintenance en la raíz de WordPress.
Si la causa subyacente fue permisos o espacio en disco, arréglalo primero o volverá a ocurrir en el siguiente intento.

9) ¿Por qué fallan las actualizaciones después de restaurar desde backup?

Las restauraciones a menudo cambian propiedad, permisos o eliminan ACLs. De repente PHP no puede escribir en wp-content.
Tras cualquier restauración, ejecuta una validación de permisos/propiedad y corrígela según tu modelo elegido.

10) ¿Cuál es la monitorización mínima para evitar sorpresas en las actualizaciones?

Alerta sobre uso de disco y uso de inodos para el montaje que contiene WordPress, y por separado para /tmp si es tmpfs.
También alerta sobre errores de PHP-FPM que contengan “Permission denied” y “No space left on device” para ver patrones antes de la próxima ventana de actualización.

Siguientes pasos que puedes hacer hoy

Si tu actualización de WordPress falló, no lo trates como un problema místico de WordPress. Casi siempre es una de tres cosas:
el proceso no puede escribir, el disco está lleno (o sin inodos), o los recursos temporales/unzip están limitados.

  1. Haz el diagnóstico rápido: disco/inodos, /tmp, usuario PHP, logs.
  2. Arregla la superficie más pequeña: haz wp-content escribible para la identidad correcta; evita hacer todo el árbol escribible.
  3. Estandariza tu modelo: web-owned, grupo compartido o CI-only. La propiedad mixta es futuras caídas con gabardina.
  4. Operationaliza las actualizaciones: WP-CLI en ventanas de mantenimiento, logs capturados y un plan de rollback que no implique pánico.
← Anterior
Contenedores vs VMs: qué perfil de CPU gana para cada caso
Siguiente →
Cola de Postfix atascada: flujo seguro de limpieza (sin pérdida de datos)

Deja un comentario