Buscar en la documentación

Docker

Contenedores de Trazo, cómo se levantan y cómo conviven con otros proyectos.

8 min de lecturaVer en GitHub

Trazo corre completo en contenedores. En local basta con:

docker compose up -d

La aplicación queda en http://localhost:8088 y los correos de prueba en http://localhost:8025 (Mailpit). No hace falta crear un .env: los valores por defecto sirven para local. Si se necesita cambiar un puerto o una credencial local, se copia .env.example a .env y se ajusta.

Servicios

flowchart LR
  U[Navegador] -->|:8088| G[gateway · Nginx]
  G -->|/api, /up| A[app · PHP-FPM]
  G -->|/| F[frontend · Vue]
  A --> M[(mysql)]
  A --> R[(redis)]
  W[worker] --> R
  W --> M
  S[scheduler] --> M
  G -->|/ws| V[reverb · websockets]
  W -->|avisos| V
  I[init · migraciones] --> M
  B[backup · respaldo diario] --> M
  W -->|SMTP| P[mailpit]

  subgraph data [red interna data]
    M
    R
    B
  end
ServicioImagenFunción
gatewaynginx-unprivileged 1.30Única entrada pública. /api y /up van a Laravel; el resto, al frontend. Agrega las cabeceras de seguridad.
apptrazo/api (PHP 8.4 FPM)API de Laravel 13
inittrazo/apiCorre una vez antes que la API: migraciones y seeders. Si falla, la API no arranca.
workertrazo/apiProcesa las colas
schedulertrazo/apiTareas programadas
reverbtrazo/apiWebsockets del tiempo real. Sin puerto propio: se llega por el gateway en /ws (Tiempo real)
frontendtrazo/webEl build servido por Nginx sin root. En el modo desarrollo, Vite con recarga en caliente.
mysqlMySQL 8.4Base de datos (trazo y trazo_test para pruebas)
redisRedis 8.4Caché (base 1), colas (base 0) y contadores de seguridad: límites de frecuencia y bloqueos del login (base 2)
backupMySQL 8.4Respaldo diario de la base, comprimido y cifrado, con 7 días de historia (Respaldos)
mailpitMailpitBuzón de correo local (solo en local)
zapOWASP ZAP 2.16Escaneo de seguridad. Perfil opcional: no arranca con up -d; se corre a mano (ZAP)
phpdocphpDocumentor 3.10Documentación de funciones del backend. Perfil opcional docs, sin red (ADR-0065)

Archivos

ArchivoPara qué
docker-compose.ymlBase común a todos los entornos
docker-compose.override.ymlLocal: se carga solo. Agrega Mailpit y los perfiles opcionales zap y docs, sin cambiar las imágenes.
docker-compose.dev.ymlModo desarrollo, opcional: monta el código, usa Vite y trae las herramientas de prueba (Dos modos).
docker-compose.prod.ymlDespliegue: secretos obligatorios, sin puertos y detrás del proxy del servidor (despliegue)
docker/php/Imagen de Laravel, configuración de PHP-FPM, arranque y healthcheck
docker/frontend/Imagen del frontend y su servidor estático
docker/nginx/Configuración del gateway y cabeceras de seguridad
docker/mysql/init/Creación de la base de pruebas en el primer arranque
docker/mysql/backup.shRespaldo diario y restauración (Respaldos)

Dos modos

ModoCómo se levantaPara qué
Final (por defecto)docker compose up -d --buildVer Trazo como lo verá quien lo use: el build de Vue con la CSP estricta y la API con el código dentro de la imagen. Es rápido también en Windows.
Desarrollodocker compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.dev.yml up -d --buildTrabajar en el código: se monta desde la carpeta del equipo, Vite recarga al guardar y trae Pest, Pint, Larastan y PCOV.
  • Los dos comparten la base, Redis, las claves y los respaldos: cambiar de modo no borra nada.
  • En el modo final, un cambio en el código se ve solo después de reconstruir (--build).
  • Las pruebas del back (composer test, check y coverage) y la guía visual (/guia-visual) solo existen en el modo desarrollo.
  • Para dejar el modo desarrollo fijo, en un .env: COMPOSE_FILE=docker-compose.yml:docker-compose.override.yml:docker-compose.dev.yml (en Windows, separado con ;).

Medido en Windows con Docker Desktop: la pantalla de ingreso carga en ~0,13 s en el modo final y en ~3,6 s en el de desarrollo, y la API responde en 20–80 ms frente a 0,4–1,5 s (ADR-0068).

Decisiones de diseño

  • Un solo origen. El navegador habla solo con el gateway, así que no hay CORS y la superficie expuesta es mínima.
  • Red interna para datos. MySQL y Redis están en la red data, marcada como interna: no tienen puertos publicados ni salida a internet.
  • Una imagen para cinco servicios. app, init, worker, scheduler y reverb usan la misma imagen; no se repite configuración y todos ejecutan exactamente el mismo código.
  • Sin root. PHP-FPM, el frontend y el gateway corren con usuarios sin privilegios.
  • Versiones fijas. Todas las imágenes base tienen versión exacta.
  • Builds multi-etapa. La imagen de producción no lleva Composer ni herramientas de compilación.
  • Arranque ordenado. Cada servicio espera a que sus dependencias estén sanas (healthcheck), no solo iniciadas.
  • Configuración de MySQL como parámetros. Charset utf8mb4_0900_ai_ci, zona horaria UTC, registro de consultas lentas (más de 1 s) e índices FULLTEXT desde 3 letras se pasan al arrancar el contenedor. MySQL descarta por seguridad los archivos .cnf montados con permisos amplios, que es lo que ocurre en Windows y macOS.

Entorno local en Windows y macOS

El código se monta desde el disco del equipo, y en estos sistemas llega al contenedor sin permiso de escritura para el usuario sin privilegios. Por eso, en local, init arranca como root solo para ajustar los permisos de vendor/, storage/ y bootstrap/cache/; la instalación de dependencias y las migraciones corren como www-data.

El script de arranque local (docker/php/dev-init.sh) también se monta desde el repositorio en vez de copiarse a la imagen. Docker no reconstruye una imagen que ya existe, así que con el script copiado, un equipo con una imagen anterior seguía usando la versión vieja: migraba pero no cargaba los datos de prueba. Montado, siempre corre la versión actual.

Las dependencias de PHP (vendor/) no se leen desde la carpeta del equipo sino desde un volumen de Docker (trazo_vendor). PHP abre cientos de archivos de vendor/ en cada petición, y leerlos a través de la carpeta compartida de Windows es lento: con el volumen, cada petición bajó de varios segundos a menos de uno, y arrancar Laravel desde la consola, de 26 s a poco más de 1 s. init instala las dependencias en ese volumen con composer install. La carpeta backend/vendor/ del equipo, si existe, ya no la usa ningún contenedor; sirve solo para el editor.

Para reinstalar las dependencias desde cero: docker compose -p trazo down, docker volume rm trazo_vendor y docker compose up -d.

Si los builds fallan con errores como no such host o TLS: unspecified error, el problema suele ser un DNS inestable en la red local. Se resuelve agregando "dns": ["1.1.1.1", "8.8.8.8"] en Docker Desktop → Settings → Docker Engine.

Gateway y contenedores que se recrean

El gateway vuelve a consultar el DNS interno de Docker (resolver 127.0.0.11 y resolve en cada upstream). Así, si la API o el frontend se recrean con otra IP, el gateway los sigue encontrando sin reiniciarse.

Frontend en desarrollo

  • Vite corre con un límite de 1 GB de memoria (el Nginx de producción usa 128 MB).
  • En Windows y macOS los cambios de archivos montados no llegan al contenedor como eventos, así que Vite los revisa por sondeo (VITE_USE_POLLING).
  • Mientras Vite arranca (unos segundos), el gateway puede responder 502: basta con recargar.
  • Al arrancar, Vite compila el layout y todas las pantallas (server.warmup), así la primera visita a cada una no espera (ADR-0067).

Llave de cifrado (APP_KEY)

La llave cifra datos sensibles, como los secretos del MFA y los correos que esperan en la cola. Si se pierde, esos datos no se pueden recuperar.

  • En local se genera sola la primera vez y se guarda en el volumen trazo_app-key. No hay que hacer nada. Los comandos que entran con docker compose exec (sin pasar por el arranque del contenedor) la leen de ese mismo volumen (ADR-0066).
  • En producción debe definirse en el entorno del servidor y respaldarse. Si falta, los contenedores se niegan a arrancar.
  • Antes de cambiarla, hay que esperar a que la cola esté vacía (Sistema → Salud). Los correos pendientes se cifraron con la llave anterior y el worker ya no podría leerlos (ADR-0062).

Convivencia con otros proyectos

En la misma máquina pueden correr otros proyectos con Docker. Para no interferir:

  • El proyecto se llama siempre trazo: contenedores, redes y volúmenes llevan ese prefijo.
  • Solo se publican dos puertos en el host, ambos configurables: 8088 (aplicación) y 8025 (Mailpit, solo local).
  • Cada contenedor tiene límites de CPU y memoria.
  • Todos los comandos se limitan al proyecto (docker compose -p trazo ...). No se usan comandos globales como docker system prune o docker volume prune.

Comandos frecuentes

ParaComando
Levantar tododocker compose up -d
Ver el estadodocker compose ps
Ver los logs de la APIdocker compose logs -f app
Ejecutar un comando de Laraveldocker compose exec app php artisan <comando>
Detener sin borrar datosdocker compose down
Borrar también los datos de Trazodocker compose down -v (solo afecta volúmenes de Trazo)
Pruebas del back con coberturadocker compose exec app composer coverage (solo en el modo desarrollo, que trae PCOV)
Vaciar los límites de frecuencia y bloqueos (solo desarrollo)docker compose exec app php artisan trazo:reset-limits
Escanear con ZAPVer Escaneo con OWASP ZAP
Ver o hacer respaldosVer Respaldos
Generar la documentación de funciones del backdocker compose --profile docs run --rm phpdoc (queda en docs-build/backend)

Dónde está en el código