Zento: el CLI que construí para dejar de pelear con N entornos Magento
Cuatro decisiones de diseño detrás de un CLI en Go para entornos Magento locales: config compilada en capas, un solo ingress con TLS real, Xdebug sin impuesto y onboarding idempotente.
Hubo una semana que me hizo decidir construir una herramienta. Tres proyectos Magento activos a la vez, de varios clientes, cada uno con su versión de PHP, su combinación de servicios y su carpeta docker-compose clonada de algún proyecto anterior y mutada a mano. El lunes, dos tiendas peleándose por el puerto 443. El miércoles, un bug que solo pasaba en un proyecto porque su nginx.conf había divergido silenciosamente del resto hacía meses. El jueves, medio día perdido levantando un proyecto en el que no trabajaba desde hacía un trimestre.
Ninguno de esos problemas era interesante. Todos eran evitables. De esa semana salió Zento: un CLI en Go que administra entornos Magento locales sobre Docker, y que hoy es la herramienta con la que levanto cualquier proyecto, nuevo o heredado, en minutos. Este post no es un tutorial (la herramienta todavía no es pública, sobre eso al final): es un recorrido por las cuatro decisiones de diseño que más cambiaron mi día a día, contadas para que puedas robarte las ideas aunque nunca uses Zento.
El problema: N tiendas a la vez
El entorno local de un solo proyecto Magento es un problema resuelto. El problema real aparece con el plural: cuando trabajas en varios proyectos por semana, cada uno con su versión de Magento (y por lo tanto de PHP), su motor de búsqueda, su Varnish sí o no, y su base de datos de varios gigas.
Copiar la carpeta de Docker del proyecto anterior y ajustarla es la solución por defecto, y tiene dos costos que crecen con cada proyecto:
- Divergencia silenciosa. Cada copia es un fork sin upstream. El fix de FPM que hiciste en el proyecto A nunca llega al B; seis meses después tienes cinco versiones de
nginx.confy ninguna es “la buena”. - Colisiones de recursos. Dos proyectos no pueden publicar el mismo puerto. Empiezas a inventar esquemas (
8081,8082,33061…) que nadie recuerda y que terminan en un README desactualizado.
Y hay un tercer costo más sutil: el onboarding. Volver a un proyecto dormido, o incorporar a alguien nuevo, significaba una lista manual de pasos que vivía a medias en un wiki y a medias en la memoria de alguien. Cada paso manual es un lugar donde el proceso se rompe distinto cada vez.
Por qué otra herramienta
No partí de cero ni pretendo haber inventado el género. markshust/docker-magento es la referencia del ecosistema y fue mi punto de partida durante años: para un proyecto, funciona muy bien. Warden atacó el multi-proyecto antes que yo, con un proxy global que respeto mucho. Y DDEV resuelve el caso genérico de PHP con mucha madurez.
Las ideas sueltas existían. Lo que no encontré fue una herramienta que modelara el problema como yo lo vivía: proyectos cuya config Docker está versionada junto al código pero no copiada, entornos internos (dev, staging, réplicas de producción) que son workspaces independientes con su propio clone de git y su propia base de datos, y un onboarding que sea un comando y no un documento. Esa brecha entre “existen piezas” y “existe el modelo completo” es la única justificación honesta que conozco para construir tooling propio.
Construir una herramienta interna se justifica cuando el modelo mental que necesitas no existe en el ecosistema; no cuando te falta una feature, porque las features se contribuyen.
Decisión 1: la config se compila, no se copia
La decisión que define a Zento: ningún archivo que lea Docker Compose se edita a mano. El proyecto versiona una fuente (.docker_config/: templates de Nginx/PHP/Varnish con variables, archivos .env por capa, definición de entornos), y un paso de compilación la convierte en un artefacto generado (.compiled/, fuera de git) que es lo único que Compose toca.
La compilación mezcla cuatro capas de variables, en orden de prioridad creciente:
- Base: defaults para todos los entornos (
env/php.env). - Override por entorno: lo que dev o staging cambian (
env/dev/php.envconxdebugactivo yopcacheapagado, por ejemplo). - Servicios: variables generadas automáticamente al activar servicios opcionales (
zento service opensearch on). - Computadas: las que calcula el CLI (rutas del código fuente, nombre de base de datos, volúmenes).
Lo que esto compra no es elegancia: es que la divergencia se vuelve imposible de ocultar. La config compartida vive en templates comunes; lo que un proyecto cambia queda explícito en su capa de override, en un diff de pocas líneas, y no enterrado en la copia número cinco de un archivo de 300 líneas.
Sobre esa base se montan los entornos como workspaces. Cada entorno de un proyecto tiene su propio clone independiente del repositorio (cualquier rama, incluso la misma que otro entorno), su base de datos dedicada, sus servicios y sus scripts. Y un entorno puede heredar de otro con parent:; todo campo no definido baja en cascada del ancestro:
# .docker_config/environments.yaml
default_env: dev
environments:
production:
branch: main
dedicated_db: true
dev:
parent: production # hereda config, servicios y scripts...
branch: develop # ...pero trabaja sobre otra rama
shared_media: true # y comparte pub/media para no duplicar gigas
La herencia es por capas también en los .env: si el php.env de un hijo contiene solo PHP_MEMORY_LIMIT=2G, conserva todo lo demás del padre. “Mismo entorno que producción pero con más memoria y otra rama” se declara en tres líneas, y se entiende leyéndolas.
Decisión 2: un solo ingress para todo
La regla de red de Zento es radical: ningún contenedor publica puertos al host por defecto. Ni la web, ni la base de datos, ni Redis. El punto de entrada es un único Traefik compartido entre todos los proyectos, que vive en ~/.zento/proxy/, arranca solo cuando el primer proyecto hace zento start y se apaga cuando se detiene el último.
El detalle que más me gusta de esta pieza es cómo maneja TLS: Traefik enruta por SNI con passthrough, es decir, mira el hostname del handshake y pasa los bytes sin descifrarlos. Cada proyecto termina TLS en su propio Nginx, con un certificado wildcard firmado por una CA local por usuario (~/.zento/ca/). Confías en esa CA una sola vez por máquina (zento certs trust la instala en el sistema y en el almacén del navegador) y a partir de ahí todos los proyectos, presentes y futuros, tienen HTTPS real sin warnings. El proxy ni siquiera tiene certificados: no hay nada que renovar ni sincronizar en el punto central.
¿Y los servicios TCP, cuando quieres conectar un cliente de base de datos desde el host? Opt-in explícito:
zento expose db # publica db en 127.0.0.1:<puerto derivado del proyecto>
zento expose # lista lo expuesto
zento unexpose db # deja de publicar
El puerto se deriva del proyecto, así dos proyectos nunca chocan y nadie mantiene una tabla de puertos en un README. La colisión del puerto 443 con la que abrí este post no es que se resuelva: es que ya no puede ocurrir, porque nadie compite por el host.
Decisión 3: Xdebug sin pagar el impuesto
Xdebug tiene un costo de ejecución alto incluso cuando no hay sesión de debugging activa, y la respuesta habitual es binaria: o lo tienes activado y todo el entorno se arrastra, o lo tienes apagado y activarlo implica reiniciar contenedores justo cuando encontraste el bug.
Zento levanta dos pools de PHP-FPM idénticos, uno con Xdebug y otro sin, y deja que Nginx elija por request, según la cookie que ya usan las extensiones de navegador tipo Xdebug Helper:
# Sin cookie -> FPM sin Xdebug. Con cookie -> FPM con Xdebug.
map $cookie_XDEBUG_SESSION $fastcgi_pass {
"" fastcgi_backend; # socket del pool sin xdebug
default fastcgi_backend_xdebug; # socket del pool con xdebug
}
El navegador donde activaste la extensión debuggea; el resto del tráfico (el frontend que recargas, los crons, tus compañeros si comparten el entorno) sigue pasando por el pool rápido. Activar y desactivar es un clic, sin reiniciar nada. Es la sección más corta del post y probablemente la idea más fácil de adoptar mañana mismo en cualquier stack: la heredé del ecosistema, la afiné, y no concibo volver atrás.
Decisión 4: el onboarding es un pipeline idempotente
La medida de éxito que me puse para Zento fue concreta: de git clone a tienda navegable con datos reales, sin tocar nada a mano. La secuencia completa para sumarse a un proyecto existente:
zento init my-store --git-repo git@gitlab.example:org/magento.git
cd my-store
zento start --build # levanta el stack
zento before-import # composer install, crea la DB, genera env.php
zento db-cloud-dump # baja e importa el dump (o: zento db-import dump.sql.gz)
zento after-import # setup:upgrade, URLs a local, reindex, cache flush
zento doctor # verifica que todo quedó sano
Dos propiedades hacen que esto funcione en el mundo real y no solo en la demo:
- Idempotencia.
before-importyafter-importson re-ejecutables: si el dump falla a mitad de descarga osetup:upgraderevienta por un módulo, corriges y vuelves a correr el mismo comando. Un pipeline de onboarding que no es idempotente es una lista de pasos manuales con disfraz de automatización. - Hooks por proyecto. Cada comando dispara config-scripts
pre-ypost-si existen en el repo del proyecto. Ahí vive lo específico (sanitizar datos, configurar credenciales sandbox de pagos, ajustes de admin) sin tocar el CLI. La herramienta define el esqueleto del proceso; cada proyecto inyecta su carne. Esa frontera es lo que evita que el CLI acumuleif (proyecto == X)hasta morir.
El doctor final no es decorativo: chequea contenedores, sockets, DNS, certificados y estado de Magento, y convierte “me funciona, creo” en una lista verificable. Cuando el onboarding es un comando, volver a un proyecto dormido deja de tener costo de activación, y esa fricción que desaparece cambia hasta qué trabajos aceptas tomar.
Lo que quedó fuera
Cuatro decisiones no agotan la herramienta. Quedaron fuera, cada una para su propio espacio: el pipeline de deploy con fases personalizables, la reescritura de URLs para multisite al importar dumps, los servicios opcionales por profiles de Compose (Varnish, OpenSearch, RabbitMQ, MailCatcher se activan con un comando y todos los subcomandos los respetan), las imágenes propias de PHP 8.1–8.4 con el profiler SPX horneado, y un server MCP para que los agentes de IA operen los entornos con las mismas garantías que un humano. De varias de estas vale un post propio.
Cierre
Zento no es la parte interesante de mi trabajo: es lo que hace que la parte interesante empiece antes. Esa es la tesis que defendería con cualquier tooling interno: se justifica cuando el modelo mental que necesitas no existe en el ecosistema, y se amortiza en cada onboarding, cada cambio de contexto y cada bug que ya no puede existir por construcción. Las cuatro decisiones de este post (config compilada, ingress único, debugging por request, pipelines idempotentes) valen fuera de Magento y fuera de Docker; son la parte robable.
Y la promesa: Zento va a ser open source. No tiene fecha: quiero pulir la documentación y el camino de instalación antes de que tenga usuarios que no me conozcan. Pero la decisión está tomada. Si quieres enterarte cuando pase, suscríbete al RSS o escríbeme: también me sirve saber qué parte te interesa más, porque eso ordena qué documento primero.
Mientras tanto, la serie de war stories de Magento en producción sigue: la próxima es cómo dar tres métodos de entrega simultáneos (retiro en sucursal, same-day y envío estándar) sobre MSI, con reservas de stock por ventana horaria.