Saltar al contenido
Facturador

Scripts de optimización Docker (Pro v9.5)

Qué cubre:

  • cuál de los dos scripts usar y cuándo;
  • cómo descargarlos con curl y 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.sh cuando el cliente tiene cosas propias que deben seguir: ~/.ssh montado para git pull, un zz-custom.ini, puertos fijos (por ejemplo 8000:8000), un command propio.
  • upgrate_docker_config.sh cuando 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.conf o custom.cnf desde otra carpeta, update no sirve: conserva ese montaje y además agrega el de docker-config/, y docker compose config falla con Duplicate mount point. El script aborta antes de tocar nada, pero hay que usar upgrate.

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.yml en el formato de install.sh: servicios nginx_{N}, fpm_{N}, mariadb_{N}, redis_{N}, scheduling_{N}, supervisor_{N} y, opcional, centrifugo_{N}.
  • Volúmenes mysqldata{N} y redisdata{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
  • -f hace que curl falle 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.sh y bash -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) o actual -> 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 cliente si 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-db solo imprime el comando up; usa los dos comandos de arriba (stop -t 300 primero).

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

  1. 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 en apt, lo respeta y no lo duplica. Solo Debian/Ubuntu y solo si Docker se instaló con docker-ce.
  2. Detecta la instalación desde el compose actual: {N}, {NOMBRE}, {DOMINIO}, ruta de proxy/fpms, imágenes y si hay Centrifugo. Avisa de servicios que no conoce, que se perderían al regenerar.
  3. Calcula los valores según RAM y vCPU (sección 8).
  4. Pide confirmación y muestra el resumen.
  5. Genera docker-config/ y la excluye del repo (.git/info/exclude).
  6. supervisor.conf: lo crea desde supervisor.conf.example si falta; si usa docker exec, lo reemplaza por el ejemplo (con respaldo); agrega la interfaz de control (unix_http_server) si falta.
  7. Regenera el compose en docker-compose.yml.new, lo valida con docker compose config -q y recién entonces lo reemplaza, dejando docker-compose.yml.bak-AAAA-MM-DD-HHMM.
  8. Descarga imágenes y recrea fpm, scheduling, supervisor, redis, nginx y centrifugo.
  9. MariaDB (si no se usa --skip-db): innodb_fast_shutdown=0, apagado limpio con hasta 5 minutos (stop -t 300), recreación y espera.
  10. Laravel: optimize:clear → config:cache → route:cache (el orden importa: primero limpiar, después cachear) y queue:restart. Si route:cache falla, hace route:clear y el sistema sigue sin caché de rutas.
  11. 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 (redispassword es 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 .env y 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 point con update_docker_config.sh: el cliente ya montaba su propio zz-opcache.ini, zz-pool.conf o custom.cnf desde otra carpeta y el script conservó ese montaje además del nuevo. Usa upgrate_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 .env del 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.list y también en /etc/apt/sources.list.d/docker.list. Si ya está en sources.list, puedes borrar docker.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 --yes aborta si el compose perdería configuración del cliente.
  • upgrate_docker_config.sh --yes no 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-run previo en cada servidor nuevo.