Scripts de optimización Docker (Pro v9.5)
Qué cubre:
- cuál de los dos scripts usar y cuándo;
- cómo descargarlos con
curly cómo ejecutarlos paso a paso en producción; - opciones, variables y valores que calcula;
- los problemas que ya aparecieron en servidores reales y cómo resolverlos.
Los scripts aplican a una instalación que ya existe: MariaDB, PHP-FPM, OPcache, Redis, límites de supervisor/scheduling, rotación de logs y caché de rutas de Laravel. Están en el snippet: https://git.buho.la/-/snippets/84.
0. Marcadores usados en este manual
| Marcador | Qué es | Ejemplo |
|---|---|---|
{N} |
Número de la instalación (sufijo de servicios y volúmenes) | 1 → servicio fpm_1, volumen mysqldata1 |
{NOMBRE} |
Dominio con guiones bajos (container_name) |
midominio_pe → contenedor fpm_midominio_pe |
{DOMINIO} |
Dominio de la instalación | midominio.pe |
{RUTA} |
Carpeta del proyecto en el servidor (donde está docker-compose.yml) |
/root/midominio.pe |
1. Qué scripts hay y cuál usar
| Script | Modo | Qué hace con el docker-compose.yml del cliente |
|---|---|---|
update_docker_config.sh |
Conserva | Regenera el compose, pero conserva los volúmenes propios, los puertos publicados y el command de supervisor/centrifugo. Si algo del cliente se perdería, lo lista y pide confirmación (con --yes aborta). |
upgrate_docker_config.sh |
Upgrade | Regenera el compose desde la plantilla. No conserva volúmenes, puertos, variables, command ni servicios propios del cliente: los lista como "se ELIMINAN" y sigue, sin pedir confirmación. |
El script de upgrade se llama
upgrate_docker_config.sh(así está en el snippet).
Cuál elegir
update_docker_config.shcuando el cliente tiene cosas propias que deben seguir:~/.sshmontado paragit pull, unzz-custom.ini, puertos fijos (por ejemplo8000:8000), uncommandpropio.upgrate_docker_config.shcuando la instalación es estándar, o ya tenía afinaciones antiguas que quieres reemplazar por las del manual (por ejemplo montajes/root/config/php/zz-opcache-...ini).
⚠️ Si el cliente ya monta su propio
zz-opcache.ini,zz-pool.confocustom.cnfdesde otra carpeta,updateno sirve: conserva ese montaje y además agrega el dedocker-config/, ydocker compose configfalla con Duplicate mount point. El script aborta antes de tocar nada, pero hay que usarupgrate.
Excepción en los dos: Redis. El bloque redis_{N} actual se conserva tal cual si la imagen no es redis:* (por ejemplo rash07/redis-buhoprinter:1.0) o si el .env tiene REDIS_PASSWORD con valor (distinto de vacío o null). La plantilla trae Redis sin contraseña y dejaría la app con WRONGPASS/NOAUTH. Solo se le agrega la rotación de logs. Ver sección 10.
2. Requisitos
- Debian o Ubuntu, como
root. - Carpeta del proyecto con el
docker-compose.ymlen el formato deinstall.sh: serviciosnginx_{N},fpm_{N},mariadb_{N},redis_{N},scheduling_{N},supervisor_{N}y, opcional,centrifugo_{N}. - Volúmenes
mysqldata{N}yredisdata{N}declarados. Si no los encuentra, el script aborta para no crear una base de datos vacía. - Docker Engine 24 o mayor y plugin de compose 2.20.0 o mayor. El script lo revisa y, si hace falta, lo instala o actualiza desde el repo oficial de Docker (ver sección 7).
- Snapshot del VPS (o al menos un dump de las bases) antes de aplicar.
3. Descargar con curl
Desde la carpeta del proyecto ({RUTA}):
cd {RUTA}
# Modo "conserva": mantiene volúmenes, puertos y commands propios del cliente
curl -fL --show-error \
"https://git.buho.la/-/snippets/84/raw/main/update_docker_config.sh" \
-o update_docker_config.sh \
&& chmod +x update_docker_config.sh && ./update_docker_config.sh
# Modo "upgrade": regenera el compose desde la plantilla
curl -fL --show-error \
"https://git.buho.la/-/snippets/84/raw/main/upgrate_docker_config.sh" \
-o upgrate_docker_config.sh \
&& chmod +x upgrate_docker_config.sh && ./upgrate_docker_config.sh
-fhace quecurlfalle si la URL responde con error (así no se guarda una página de error como si fuera el script).- Si cambia el nombre de algún archivo dentro del snippet, la URL siempre sigue el formato
.../snippets/84/raw/main/<nombre-del-archivo>. - Para ver qué hace antes de ejecutarlo:
less upgrate_docker_config.shybash -n upgrate_docker_config.sh(revisa la sintaxis sin ejecutar).
Para dejar el script en otra carpeta y apuntarlo a una instalación, se le pasa la ruta como último argumento:
./upgrate_docker_config.sh --dry-run /root/midominio.pe
4. Flujo recomendado en producción
cd {RUTA}
# 1. Dry-run: muestra lo detectado y calculado; deja docker-compose.yml.new y no aplica nada
./upgrate_docker_config.sh --dry-run
# 2. Revisar qué cambia respecto al compose actual
diff docker-compose.yml docker-compose.yml.new
En el resumen del dry-run revisa:
- Imágenes: cada una aparece como
(sin cambio)oactual -> nueva ⚠️ CAMBIA. Un cambio de imagen de FPM puede ser un cambio de versión de PHP. - Líneas que se eliminan o se pierden: volúmenes, puertos, variables o servicios del cliente que no quedarán.
- Redis: debe decir
SE CONSERVA el bloque actual del clientesi tiene imagen propia o contraseña. - Valores calculados para el servidor (buffer pool,
max_children, límites de supervisor).
# 3. Snapshot del VPS (o dump de las bases) ANTES de aplicar
# 4. En horario de poco uso. Los servicios caen unos 15 segundos.
# Se deja MariaDB para después con --skip-db
./upgrate_docker_config.sh --skip-db
# 5. Verificar (ver sección 11): login de un tenant, listado de comprobantes, emitir una boleta
# 6. Con calma: recrear MariaDB con apagado limpio (redimensiona el redo log)
docker compose stop -t 300 mariadb_{N}
docker compose up -d --force-recreate mariadb_{N}
docker compose logs mariadb_{N} --tail 50
El script, cuando se corre sin
--skip-db, hace el apagado limpio de MariaDB solo. Con--skip-dbsolo imprime el comandoup; usa los dos comandos de arriba (stop -t 300primero).
Al terminar el paso 6, MariaDB debe mostrar en el log Resizing redo log from ... to ... bytes sin errores.
5. Opciones
| Opción | Qué hace |
|---|---|
--dry-run |
Solo muestra lo detectado y calculado y deja docker-compose.yml.new. No instala ni recrea nada. |
--yes |
No pide confirmaciones. En update, si el compose perdería configuración del cliente, aborta en vez de continuar. |
--skip-db |
No recrea MariaDB (se aplica después, en horario de baja carga). |
--no-app |
No ejecuta los comandos artisan (cachés, queue:restart). |
--composer |
Ejecuta composer install --no-dev --optimize-autoloader. |
--migrate |
Ejecuta migrate --force y tenancy:migrate --force. |
--no-docker-upgrade |
No instala ni actualiza Docker ni el plugin de compose; solo verifica los mínimos y aborta si no se cumplen. |
{RUTA} |
Carpeta del proyecto, si no se ejecuta desde ella. |
6. Variables de entorno (sobrescriben los cálculos)
| Variable | Para qué | Ejemplo |
|---|---|---|
BP_GB |
Fuerza el innodb_buffer_pool_size (GB) |
BP_GB=2 ./upgrate_docker_config.sh |
MEM_PROC |
RAM (MB) por proceso PHP-FPM, en vez de medirla | MEM_PROC=180 |
INSTALLS |
Nº de instalaciones que comparten el servidor | INSTALLS=3 |
REDIS_MAXMEM |
Tope de memoria de Redis | REDIS_MAXMEM=1024mb |
FPM_IMAGE, SCHED_IMAGE, SUP_IMAGE |
Imágenes a usar. Para no cambiar la imagen actual de un cliente | FPM_IMAGE=rash07/php-fpm:8.2 SCHED_IMAGE=rash07/scheduling:8.2 ./update_docker_config.sh |
Valores por defecto de las imágenes: buhola/php-fpm:8.2.1, buhola/scheduling:8.2.1, rash07/supervisor-php:8.2.
7. Qué hace el script, paso a paso
- Docker y compose. Exige Docker Engine ≥ 24 y compose ≥ 2.20.0. El plugin se deja siempre en la última versión estable del repo oficial de Docker (no reinicia nada). Si el Engine es menor a 24, lo actualiza reiniciando el daemon (pide confirmación, salvo
--yes). Si ya hay un repo de Docker enapt, lo respeta y no lo duplica. Solo Debian/Ubuntu y solo si Docker se instaló condocker-ce. - Detecta la instalación desde el compose actual:
{N},{NOMBRE},{DOMINIO}, ruta deproxy/fpms, imágenes y si hay Centrifugo. Avisa de servicios que no conoce, que se perderían al regenerar. - Calcula los valores según RAM y vCPU (sección 8).
- Pide confirmación y muestra el resumen.
- Genera
docker-config/y la excluye del repo (.git/info/exclude). supervisor.conf: lo crea desdesupervisor.conf.examplesi falta; si usadocker exec, lo reemplaza por el ejemplo (con respaldo); agrega la interfaz de control (unix_http_server) si falta.- Regenera el compose en
docker-compose.yml.new, lo valida condocker compose config -qy recién entonces lo reemplaza, dejandodocker-compose.yml.bak-AAAA-MM-DD-HHMM. - Descarga imágenes y recrea
fpm,scheduling,supervisor,redis,nginxycentrifugo. - MariaDB (si no se usa
--skip-db):innodb_fast_shutdown=0, apagado limpio con hasta 5 minutos (stop -t 300), recreación y espera. - Laravel:
optimize:clear→config:cache→route:cache(el orden importa: primero limpiar, después cachear) yqueue:restart. Siroute:cachefalla, haceroute:cleary el sistema sigue sin caché de rutas. - Verificación: contenedores, pool de FPM, OPcache CLI, Redis, workers y archivos de caché.
Antes de recrear, el script también comprueba si las imágenes de FPM y de supervisor traen OPcache: si FPM no lo trae agrega zend_extension=opcache; si supervisor no lo trae, no le monta zz-opcache-cli.ini.
8. Valores que calcula
Son las fórmulas de la sección 6 del Manual de configuración Docker. Si hay varias instalaciones en el servidor (contenedores mariadb_*), la RAM se reparte en partes iguales antes de calcular.
| Valor | Cálculo |
|---|---|
innodb_buffer_pool_size |
27 % de la RAM de la instalación, mín. 1 GB |
innodb_log_file_size |
buffer pool × 0,25 (entre 256 MB y 2 GB) |
tmp_table_size |
RAM ≤ 4 GB → 16M; ≤ 16 GB → 32M; mayor → 64M |
| OPcache FPM | memory_consumption = archivos PHP × 0,02 MB (múltiplo de 128, mín. 256); max_accelerated_files = archivos × 2 (mín. 30 000) |
pm.max_children |
el menor de (RAM − buffer pool − reserva) ÷ MEM_PROC y vCPU × 6 |
supervisor |
mem_limit = workers × 200 MB + 200 MB (múltiplo de 256); cpus = vCPU × 0,0625 (mín. 0,5) |
scheduling |
mem_limit 768m; cpus = las de supervisor × 0,75 (mín. 0,5) |
Redis maxmemory |
pico de uso × 2 (mín. 256 MB, múltiplo de 128); si no se puede medir, 512 MB |
MEM_PROC se mide en los procesos de FPM que estén corriendo (promedio de RSS); si no se puede, usa 150 MB. El número de workers sale de los numprocs de supervisor.conf.
9. Archivos que crea o modifica
| Archivo | Qué pasa |
|---|---|
docker-config/mariadb/custom.cnf |
Se crea o se sobrescribe. |
docker-config/php/zz-opcache.ini, zz-pool.conf, zz-opcache-cli.ini |
Se crean o se sobrescriben. |
docker-compose.yml |
Se reemplaza. Respaldo: docker-compose.yml.bak-AAAA-MM-DD-HHMM. |
supervisor.conf |
Se crea si falta; se reemplaza por el ejemplo si usa docker exec (respaldo supervisor.conf.bak-AAAA-MM-DD-HHMM); se le agrega el socket de control si falta. |
.git/info/exclude |
Se le agrega docker-config/. |
docker-compose.yml.new |
Temporal. Con --dry-run se queda: bórralo si no vas a aplicar. |
Para editar un valor después: cambia el archivo de docker-config/ y docker compose restart <servicio> (basta un restart si el archivo ya está montado).
10. Redis y supervisor.conf (casos especiales)
Redis con imagen propia o contraseña
Hay clientes con rash07/redis-buhoprinter:1.0, que toma la contraseña de REDIS_DEFAULT_PASSWORD=${REDIS_PASSWORD} y publica ${REDIS_PUBLIC_PORT}:6379. Si el compose se regenera con la plantilla estándar, Redis arranca sin esa variable y la app queda con WRONGPASS o NOAUTH (cachés, sesiones y colas fallan). Por eso los scripts conservan el bloque redis_{N} del cliente en esos casos y no calculan maxmemory.
Si el compose ya quedó roto, ver Redis con WRONGPASS.
🔒 Si Redis publica el puerto 6379 del servidor con una contraseña débil (
redispasswordes la clásica), conviene restringirlo por firewall a las IP de los clientes que lo usan, o cambiar la contraseña a la vez en el.envy en esos clientes.
supervisor.conf con formato antiguo
El script solo reemplaza supervisor.conf si usa docker exec. Un supervisor.conf antiguo con un solo programa laravel-worker y sin --queue procesa solo la cola default. El supervisor.conf.example del repo separa las colas: high,default (6 workers), reports (2) y backups (1).
Para saber si el sistema usa esas colas:
cd {RUTA}
grep -rhoE "onQueue\('[a-z_]+'\)" app modules 2>/dev/null | sort | uniq -c
Si aparecen high, reports o backups, cambia a la configuración nueva sin reiniciar el contenedor (para no cortar jobs a medias):
cp supervisor.conf supervisor.conf.bak-$(date +%F-%H%M)
cp supervisor.conf.example supervisor.conf
# En servidores de 4 GB: baja numprocs de laravel-worker-fast de 6 a 3 antes del reread
docker compose exec -T supervisor_{N} supervisorctl reread
docker compose exec -T supervisor_{N} supervisorctl update
docker compose exec -T supervisor_{N} supervisorctl status
update espera a que termine el job en curso de cada worker (puede tardar). Deben quedar laravel-worker-fast, laravel-worker-reports y laravel-worker-backup en RUNNING.
11. Verificación y rollback
El script imprime una verificación al final. A mano, desde {RUTA}:
# Contenedores
docker compose ps
# PHP-FPM: pool
docker compose exec -T fpm_{N} php-fpm -tt 2>&1 | grep -E "max_children|start_servers|max_requests"
# OPcache CLI (supervisor y scheduling): enable_cli=1 file_cache=/tmp
for s in supervisor_{N} scheduling_{N}; do
echo -n "$s: "
docker compose exec -T $s php -r 'echo "enable_cli=", ini_get("opcache.enable_cli"), " file_cache=", ini_get("opcache.file_cache"), PHP_EOL;'
done
# MariaDB (buffer pool, redo log, slow log)
docker compose exec -T mariadb_{N} sh -c 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW VARIABLES WHERE Variable_name IN (\"innodb_buffer_pool_size\",\"innodb_log_file_size\",\"slow_query_log\");"'
# Redis (si tiene contraseña, usa la de REDIS_PASSWORD del .env)
docker compose exec -T redis_{N} redis-cli ping
# Workers y cachés de Laravel
docker compose exec -T supervisor_{N} supervisorctl status
docker compose exec -T fpm_{N} ls -la bootstrap/cache/routes-v7.php bootstrap/cache/config.php
# Jobs que fallaron durante el cambio
docker compose exec -T fpm_{N} php artisan queue:failed
Después, en el navegador: login de un tenant, listado de comprobantes, emitir una boleta y login del dominio del sistema.
Rollback del compose (el script imprime el comando al terminar):
cp docker-compose.yml.bak-AAAA-MM-DD-HHMM docker-compose.yml
docker compose up -d --force-recreate
Los datos no se tocan: los volúmenes mysqldata{N} y redisdata{N} se conservan siempre.
12. Problemas frecuentes
Redis con WRONGPASS o NOAUTH después de aplicar
Síntoma: Predis\Connection\ConnectionException: WRONGPASS invalid username-password pair en cualquier artisan; config:cache y route:cache fallan; redis-cli responde NOAUTH Authentication required.
Causa: el compose se regeneró con la plantilla y el servicio redis_{N} perdió REDIS_DEFAULT_PASSWORD y/o el puerto publicado. Los scripts actuales ya lo evitan, pero si pasó con una versión anterior, restaura solo el bloque de Redis desde el respaldo de antes de la primera corrida (no uses el de una segunda corrida: ya sale roto):
cd {RUTA}
cp docker-compose.yml docker-compose.yml.pre-redisfix
awk -v bak=docker-compose.yml.bak-AAAA-MM-DD-HHMM '
BEGIN{ while((getline l < bak)>0){ if(l==" redis_1:"){p=1} else if(p && l ~ /^ [^ ]/){p=0} else if(p && l ~ /^[^ ]/){p=0}; if(p) blk=blk l "\n" } }
/^ redis_1:/{printf "%s", blk; skip=1; next}
skip && /^ [^ ]/{skip=0}
skip && /^[^ ]/{skip=0}
skip{next}
{print}' docker-compose.yml.pre-redisfix > docker-compose.yml
docker compose config -q && docker compose up -d --force-recreate redis_1
sleep 3
docker compose exec -T redis_1 redis-cli -a "$(grep -m1 '^REDIS_PASSWORD=' .env | cut -d= -f2-)" ping # PONG
Cambia redis_1 por redis_{N} si el servicio tiene otro número. Con PONG, regenera las cachés:
docker compose exec -T fpm_{N} php artisan optimize:clear
docker compose exec -T fpm_{N} php artisan config:cache
docker compose exec -T fpm_{N} php artisan route:cache
docker compose exec -T fpm_{N} php artisan queue:restart
Verifica que REDIS_PASSWORD del .env sea el mismo valor que tenía antes: lo usan la app y los clientes de Redis (por ejemplo, los de impresión).
Duplicate mount point o error de docker compose config
Duplicate mount pointconupdate_docker_config.sh: el cliente ya montaba su propiozz-opcache.ini,zz-pool.confocustom.cnfdesde otra carpeta y el script conservó ese montaje además del nuevo. Usaupgrate_docker_config.sh. No se tocó el compose original.- Variable no definida (
${CENTRIFUGO_PORT_HOST},${MYSQL_PORT_HOST}): el compose nuevo usa esas variables del.env. Agrégalas al.envdel cliente (con el puerto que hoy publica) y repite.
route:cache falla
LogicException: Unable to prepare route [...] for serialization. Another route has already been assigned name [xxx].
Dos rutas con el mismo nombre (->name('xxx')), normalmente en un módulo nuevo o modificado. Avisar a desarrollo con el nombre. El script deja el sistema sin caché de rutas (route:clear): funciona, solo más lento. Ver la sección 7 del Manual de configuración Docker.
MariaDB no arranca o tarda tras cambiar el redo log
docker compose logs mariadb_{N} --tail 50
Se redimensiona el redo log al arrancar y necesita un apagado limpio (por eso el script usa stop -t 300). Si MariaDB recibió un kill, puede fallar. Restaura el compose del respaldo, recrea mariadb_{N} y, si no vuelve, restaura el snapshot.
Avisos de apt durante la revisión de Docker
- Failed to fetch ... mirror ... No address associated with hostname: el mirror de Ubuntu del proveedor no resuelve DNS en ese servidor. No afecta al script.
- Target Packages ... configured multiple times: el repo de Docker está en
/etc/apt/sources.listy también en/etc/apt/sources.list.d/docker.list. Si ya está ensources.list, puedes borrardocker.list. - Pending kernel upgrade: es de Ubuntu, no del script. Reinicia el servidor cuando puedas.
comm: file 1 is not in sorted order
Es de una versión anterior del script en Ubuntu 26.04 (su comm es más estricto) y produce falsos avisos de líneas "perdidas" (./:/var/www/html). Descarga la versión actual del snippet.
El script falla antes de aplicar
El compose original solo se reemplaza después de validar el nuevo. Si el script se detiene antes, docker-compose.yml sigue siendo el del cliente. Lo que sí puede haber quedado: docker-config/, docker-compose.yml.new y, si usaba docker exec, el supervisor.conf reemplazado (con su .bak).
13. Ejecución desatendida
Con --yes no pregunta nada. Para no interrumpir el servicio dejando MariaDB aparte:
./update_docker_config.sh --yes --skip-db /root/midominio.pe
update_docker_config.sh --yesaborta si el compose perdería configuración del cliente.upgrate_docker_config.sh --yesno pregunta nunca: elimina lo propio del cliente y lo lista en la salida.- Recrea contenedores: no lo programes en horario de uso, y haz siempre un
--dry-runprevio en cada servidor nuevo.