Este modelo es el que corre con
make up en desarrollo local y con el chart de Helm en Kubernetes. Si usas make up, no necesitas hacer nada manual — el stack de Compose bloquea la app hasta que el runner de migración termine.Por qué un runner dedicado
El antiguo migrador in-process se ejecutaba en cada arranque de la app desde el directorio de trabajo. Bajo un contexto de seguridad de pod endurecido (
distroless:nonroot, runAsUser: 1000, readOnlyRootFilesystem: true, capabilities: drop [ALL]), esa ruta fallaba con permission denied y provocaba crash-loop en el pod.
El runner dedicado evita eso por completo:
- Las migraciones corren una sola vez, como un paso separado, antes de que arranque la app.
- La imagen de la app ya no incluye SQL de migración y nunca escribe en el esquema.
- La propia imagen del runner corre limpia bajo el mismo contexto de seguridad endurecido (ver Postura de seguridad).
Imágenes del runner
Se publican dos imágenes por release:
Ambas imágenes comparten la misma forma:
- Base:
migrate/migratev4.19.1(pineada). - Se ejecuta como
USER 65532:65532— no-root, agnóstica al UID. - Entrypoint POSIX shell que ensambla un DSN a partir de variables de entorno, ejecuta
migrate ... upy sale. - No escribe nada en disco — el progreso se guarda en la tabla
schema_migrationsde Postgres, por lo quereadOnlyRootFilesystem: truees seguro.
Contrato de entorno
Cada entrypoint acepta o bien un override de URL prebuilt o las variables
DB_* individuales. El override de URL tiene prioridad cuando está definido.
Runner de Ledger
Runner de Tracer
Forma del DSN
Cuando el entrypoint ensambla el DSN a partir de las variablesDB_*, produce:
% @ : / ? # & + space [ ]) no rompan el DSN. % se codifica primero para no doble-codificar escapes ya insertados.
El entrypoint solo registra marcadores de fase (applying onboarding migrations, applying transaction migrations, applying tracer migrations, migrations complete). Nunca imprime credenciales ni el DSN ensamblado.
Desarrollo local
make up
Desde la raíz del repositorio, make up:
- Levanta la infra y espera hasta que Postgres esté healthy.
- Arranca los servicios one-shot
ledger-migrateytracer-migrateen Compose. - Arranca cada app, que declara
depends_onsobre su servicio de migración concondition: service_completed_successfully.
Targets make en el host
Si quieres ejecutar migraciones directamente contra una base local — por ejemplo mientras desarrollas una migración nueva — cada componente expone targets de Make que usan el CLI pineado de golang-migrate y leen la configuración de conexión desde .env.
Ledger (components/ledger):
Tracer (
components/tracer):
Ejemplo: ejecutar el runner de Ledger a mano
Rara vez necesitas invocar la imagen del runner manualmente — la gate de Compose lo hace por ti. Cuando lo hagas, apunta el runner a tus bases conDB_* (o *_DATABASE_URL) y ejecútalo una vez:
0 si tiene éxito (incluido el caso idempotente sin cambios) y con no-cero si falla.
Despliegue en Kubernetes
En producción, ejecuta cada runner de migración como un Job de Kubernetes (típicamente un PreSync Job de Argo CD o un hook de Helm) que termine antes del rollout de la app. El chart de Helm ya lo conecta por ti; las secciones siguientes cubren las propiedades que debes tener presentes si escribes tus propios manifests.
Para despliegues multi-tenant, el runner es intencionadamente agnóstico al tenant: migra las bases apuntadas por su entorno, una vez. El fan-out por tenant es una preocupación del deploy — ejecuta el Job una vez por base de tenant, pasando los valores
DB_* (o el override de URL) correspondientes.Postura de seguridad
El runner está diseñado para un contexto de seguridad de pod endurecido:- No-root — la imagen fija
USER 65532:65532. - Agnóstica al UID — los archivos de migración son propiedad de root y de lectura pública, por lo que cualquier UID puede leerlos.
- Compatible con
readOnlyRootFilesystem—migrateno escribe estado en el filesystem.
runAsNonRoot: true, readOnlyRootFilesystem: true y capabilities: drop [ALL].
TLS
Los entrypoints honransslmode desde el entorno (DB_ONBOARDING_SSLMODE, DB_TRANSACTION_SSLMODE o DB_SSL_MODE), con valor por defecto disable. Los despliegues productivos deberían configurar require (o más estricto). El runner shell no reproduce la aplicación en código de TLS, la clasificación de errores ni la telemetría que las apps usan para sus propias conexiones — el TLS para las migraciones se controla enteramente vía sslmode, a cambio de un runner mínimo y sin dependencias.
Convenciones e idempotencia
- Pares up/down — cada migración tiene un archivo
.up.sqly uno.down.sql. - Idempotente — ejecutar
migrate upcuando el esquema ya está en la última versión es un no-op. Re-ejecutar el runner tras un apply parcial o completo es seguro. - CLI pineado — las imágenes del runner y los targets Make en host pinean
golang-migratev4.19.1.
Actualización desde versiones anteriores
Si venías de una release de Midaz donde la app migraba al arrancar, no se requiere migración manual de datos — el esquema en disco es el mismo. Para adoptar el nuevo modelo:
- Trae la release que incluye las imágenes del runner.
- En Kubernetes, agrega los Jobs
midaz-ledger-migrationsymidaz-tracer-migrationsa tu pipeline de deploy para que corran antes del rollout de la app. Si usas el chart oficial de Helm, esto ya está hecho. - En desarrollo local,
make upsigue funcionando igual — el stack de Compose ahora bloquea las apps sobre los nuevos servicios one-shot de migración de forma automática.
Páginas relacionadas
- Instalando Midaz — configuración local completa con
make up. - Actualizando Midaz — flujos de actualización para local y Helm.
- Estrategias de despliegue — visión general del despliegue BYOC.
- Variables de entorno de Tracer — referencia completa de configuración de Tracer.

