Cómo usar init containers en Docker Compose con pre_start
Índice de contenidos
- Puntos clave
- Qué es un init container en Docker Compose
- Qué versión de Compose necesitas y por qué pasó de v2 a v5
- Cómo ejecutar una migración de base de datos antes de arrancar la aplicación
- Cómo arreglar los permisos de un volumen para un servicio sin root
- Cómo generar y validar la configuración de nginx en dos pasos
- Qué pasa cuando un paso pre_start falla
- Cuándo se vuelve a ejecutar un paso pre_start
- Cuándo usar pre_start y cuándo un servicio de un solo uso
- Límites de pre_start en Docker Compose 5.5.1
- Preguntas frecuentes
- ¿Funciona pre_start con Docker Compose v2?
- ¿Se ejecuta pre_start con docker compose run?
- ¿Dónde veo la salida de un paso que ha fallado?
- Conclusión
- Fuentes
Docker Compose 5.3.0, publicado el 2 de julio de 2026, añade init containers con la clave pre_start: pasos que se ejecutan en contenedores efímeros, en orden, antes de arrancar el servicio. Sirven para migraciones, permisos de volúmenes y configuración generada. Compose 2.40.3 rechaza el fichero entero, y cada paso debe ser idempotente porque se repite.
Hasta julio de 2026, la receta de Docker Compose para ejecutar una migración antes de arrancar una aplicación era un servicio de un solo uso y un depends_on con la condición service_completed_successfully. Compose 5.3.0 añade init containers declarados dentro del propio servicio, en la clave pre_start. En esta guía monto tres casos reales con Compose 5.5.1: la migración de Miniflux sobre PostgreSQL, un volumen que necesita chown y una configuración de nginx generada y validada antes de arrancar. También verás qué hace una versión antigua de Compose con el fichero, cuándo se repite un paso (no coincide del todo con la documentación) y qué límites tiene hoy. La guía está disponible en inglés.
Puntos clave
pre_startexiste desde Docker Compose 5.3.0, publicado el 2 de julio de 2026. La última versión al escribir esto (14 de septiembre de 2026) es la 5.5.1, del 3 de septiembre.- Cada paso se ejecuta en un contenedor efímero con la imagen del servicio (o la que indiques), en sus mismas redes y con sus volúmenes, y tiene que salir con código 0 para que arranque el siguiente.
- Compose 2.40.3, el que trae Ubuntu 24.04, y Compose 5.2.0 rechazan el fichero con
additional properties 'pre_start' not allowed. No lo ignoran en silencio. - En mis pruebas el paso se salta mientras el servicio sigue en marcha, pero se repite tras
stopyup, tras una caída del proceso principal y cada vez que cambias un atributo del paso. Escribe pasos idempotentes. - Cambiar un paso recrea el contenedor del servicio. Si el paso nuevo falla, el servicio que funcionaba se queda parado.
- Un paso no tiene
volumespropios,per_replica: truetodavía no está admitido y los montajes:rodel servicio también son de solo lectura para el paso.
Qué es un init container en Docker Compose
Un init container es un contenedor de vida corta que se ejecuta hasta terminar antes de que arranque el contenedor principal de un servicio. Compose los modela como ganchos del ciclo de vida pre_start. A diferencia de post_start y pre_stop, que ejecutan una orden dentro del contenedor ya en marcha, cada paso de pre_start tiene su propio contenedor, creado después del contenedor del servicio y antes de arrancarlo.
La documentación de Docker sobre init containers[1] los define en una frase: "Init containers are short-lived containers that run before a service’s main container starts". Los pasos se ejecutan en el orden en que los declaras. Además, esperan a que se cumplan las condiciones de depends_on, como la de service_healthy que explicamos en la guía de healthchecks y políticas de reinicio en Docker Compose.

La especificación de Compose[2] define siete atributos por paso. El esquema JSON no admite ningún otro (salvo extensiones x-), así que no hay volumes, networks ni entrypoint por paso:
command: la orden que se ejecuta; es opcional si elCMDde la imagen ya hace lo que necesitasimage: la imagen del contenedor efímero; si la omites, se usa la del serviciouser: el usuario con el que se ejecuta la ordenworking_dir: el directorio de trabajoenvironment: variables que se suman a las del servicio o las sustituyenprivileged: ejecuta el paso en modo privilegiadoper_replica:falsepor defecto; contrueel paso se ejecutaría una vez por réplica, pero Compose 5.5.1 lo rechaza
Qué versión de Compose necesitas y por qué pasó de v2 a v5
Necesitas Docker Compose 5.3.0 o posterior en cada máquina que vaya a ejecutar el fichero. La numeración salta de la 2.40.3 (30 de octubre de 2025) a la 5.0.0, publicada el 2 de diciembre de 2025[3]. Sus notas explican el salto así:
We decided to skip 3.0.0 for next major release after docker Compose v2 to prevent (more) confusion with the obsolete docker-compose file versions 2.x and 3.x inherited from Docker Compose v1.
Es decir, el v3 y el v4 se quedaron sin usar para que nadie confundiera la herramienta con las versiones 2.x y 3.x del formato de fichero, las del campo version: que ya no hace falta escribir.
La versión que tienes depende de dónde instalaste Docker. Esto es lo que ofrecía cada origen el 14 de septiembre de 2026:
| Origen | Paquete | Versión de Compose | ¿Admite pre_start? |
|---|---|---|---|
| Debian 13 (trixie), repositorio de Debian | docker-compose |
2.26.1 | No |
Ubuntu 24.04, noble-updates |
docker-compose-v2 |
2.40.3 | No |
| Repositorio de Docker (Debian 12 y 13, Ubuntu 24.04) | docker-compose-plugin |
5.5.1 | Sí |
| Docker Desktop 4.82.0 (13 de julio de 2026) | incluido | 5.3.0 | Sí |
| Docker Desktop 4.91.0 (14 de septiembre de 2026) | incluido | 5.5.1 | Sí |
Comprueba la tuya con docker compose version. En el contenedor de desarrollo donde escribo esto sale Docker Compose version v2.40.3, y con esa versión el mismo fichero de los ejemplos falla al validar: services.web additional properties 'pre_start' not allowed, código de salida 1 y ningún contenedor creado. La 5.2.0 da el mismo error de validación. Si vas a instalar Docker desde cero, usa el repositorio de Docker como en la guía para instalar Docker en Debian: es el que trae el paquete docker-compose-plugin actual.
Para probar pre_start sin tocar el plugin del sistema, descarga el binario independiente y ejecútalo por su ruta. Es lo que hice yo, porque otros proyectos de la misma máquina dependen del Compose 2.40.3 instalado. Cambia aarch64 por x86_64 en un PC:
VERSION=v5.5.1
ARCH=aarch64
BASE="https://github.com/docker/compose/releases/download/${VERSION}"
curl -fsSLO "${BASE}/docker-compose-linux-${ARCH}"
curl -fsSLO "${BASE}/docker-compose-linux-${ARCH}.sha256"
sha256sum -c "docker-compose-linux-${ARCH}.sha256"
chmod +x "docker-compose-linux-${ARCH}"
./docker-compose-linux-${ARCH} version
La comprobación imprime docker-compose-linux-aarch64: OK y la última orden, Docker Compose version v5.5.1. Salvo donde indico otra versión, los ejemplos siguientes se ejecutaron así, con Docker Engine 29.5.2 en linux/arm64 (18 núcleos). El prefijo b3p9 que verás en las salidas es el nombre de proyecto que usé.
Cómo ejecutar una migración de base de datos antes de arrancar la aplicación
Miniflux 2.3.3 es un buen caso de prueba porque se niega a arrancar si el esquema de la base de datos no está al día, salvo que actives RUN_MIGRATIONS=1. Su documentación de línea de comandos[4] incluye miniflux -migrate, que aplica las migraciones y termina. Con pre_start esa orden sale del proceso principal y se ejecuta una vez antes de arrancarlo. El compose.yaml empieza por PostgreSQL con su healthcheck (si partes de cero, la guía para instalar PostgreSQL con Docker explica cada variable):
services:
db:
image: postgres:18.6
environment:
POSTGRES_USER: miniflux
POSTGRES_PASSWORD: tu_clave
volumes:
- pgdata:/var/lib/postgresql
healthcheck:
test: ["CMD", "pg_isready", "-U", "miniflux"]
interval: 2s
retries: 15
A continuación, en el mismo fichero, van Miniflux con el paso de migración y la declaración del volumen:
miniflux:
image: miniflux/miniflux:2.3.3
ports:
- "127.0.0.1:19981:8080"
environment:
DATABASE_URL: postgres://miniflux:tu_clave@db/miniflux?sslmode=disable
CREATE_ADMIN: "1"
ADMIN_USERNAME: admin
ADMIN_PASSWORD: tu_clave_de_admin
depends_on:
db:
condition: service_healthy
pre_start:
- command: ["/usr/bin/miniflux", "-migrate"]
volumes:
pgdata:
El paso no declara image, así que usa miniflux/miniflux:2.3.3, y hereda DATABASE_URL del servicio. Tampoco necesita esperar a PostgreSQL por su cuenta: la condición service_healthy se cumple antes de que Compose cree el contenedor del paso. Estas son las últimas líneas de up -d, que terminó con código 0:
Container b3p9-rss-db-1 Starting
Container b3p9-rss-db-1 Started
Container b3p9-rss-db-1 Waiting
Container b3p9-rss-db-1 Healthy
Container b3p9-rss-miniflux-1 Starting
Container b3p9-rss-miniflux-1 Started
La barra de progreso no menciona el paso. Donde sí aparece es en docker events: un contenedor con un nombre aleatorio (trusting_yalow) y la etiqueta com.docker.compose.hook=pre_start se crea, arranca, termina con exitCode=0 y se destruye, y justo después arranca b3p9-rss-miniflux-1. La tabla schema_version quedó en la versión 132, /healthcheck respondió 200 y docker compose ps -a listó dos contenedores, sin ningún Exited (0) de por medio.
Mi primer intento encadenaba un segundo paso con miniflux -create-admin, y falló con This is not an interactive terminal, exiting. El contenedor de un paso no tiene terminal, así que una orden interactiva no sirve. Miniflux crea el administrador desde las variables CREATE_ADMIN, ADMIN_USERNAME y ADMIN_PASSWORD al arrancar, y así quedó en el ejemplo.
Cómo arreglar los permisos de un volumen para un servicio sin root
Un volumen con nombre nuevo, montado en una ruta que no existe en la imagen, pertenece a root, y un servicio que se ejecuta con otro usuario no puede escribir en él. Lo comprobé con Alpine 3.24.1 y user: "1000:1000": el contenedor salió con código 1 y el registro dijo sh: can't create /data/prueba.txt: Permission denied. El paso que lo arregla declara su propia imagen y se ejecuta como root, aunque el servicio no lo haga:
services:
app:
image: alpine:3.24.1
user: "1000:1000"
command:
- sh
- -c
- >-
echo hola > /data/prueba.txt &&
ls -ln /data && sleep 3600
volumes:
- datos:/data
pre_start:
- image: alpine:3.24.1
user: root
command: ["chown", "-R", "1000:1000", "/data"]
volumes:
datos:
El paso ve el volumen porque Compose crea su contenedor con VolumesFrom apuntando al contenedor del servicio, según el código de pre_start.go en la 5.5.1[5]. Con el paso, el servicio escribió el fichero. docker compose logs --no-log-prefix app mostró el propietario correcto:
total 4
-rw-r--r-- 1 1000 1000 5 Sep 14 21:32 prueba.txt
Declara user en cada paso aunque parezca redundante. La especificación dice que un paso sin image hereda el usuario del servicio y que un paso sin working_dir hereda su directorio de trabajo, pero Compose 5.5.1 no lo hace. En un servicio con user: "1000:1000" y working_dir: /srv, un paso sin image ni user imprimió id=0 y pwd=/, los valores por defecto de la imagen.
Las variables de entorno del servicio sí llegaron al paso. Si quieres repasar cómo se comportan los volúmenes con nombre frente a los montajes de carpeta, lo explicamos en volúmenes y bind mounts en Docker.
Cómo generar y validar la configuración de nginx en dos pasos
Dos pasos encadenados permiten generar un fichero y comprobarlo antes de que el servicio lo lea. El primero escribe default.conf en un volumen con valores del entorno del servicio; el segundo hereda la imagen de nginx y ejecuta nginx -t:
services:
web:
image: nginx:1.30-alpine
ports:
- "127.0.0.1:19982:80"
environment:
SITIO: jacar-demo
CACHE_MAX_AGE: "3600"
volumes:
- conf:/etc/nginx/conf.d
pre_start:
- image: alpine:3.24.1
command:
- sh
- -c
- |
cat > /etc/nginx/conf.d/default.conf <<CONF
server {
listen 80;
add_header Cache-Control "max-age=$${CACHE_MAX_AGE}";
location / { return 200 "sitio=$${SITIO}\n"; }
}
CONF
- command: ["nginx", "-t"]
volumes:
conf:
El $$ evita que Compose interpole la variable al leer el fichero, así que la expande el sh del paso con el entorno heredado. curl contra el puerto 19982 devolvió HTTP/1.1 200 OK, la cabecera Cache-Control: max-age=3600 y el cuerpo sitio=jacar-demo. El segundo paso reemplaza el CMD de la imagen pero no su ENTRYPOINT, así que docker-entrypoint.sh de nginx se ejecuta también antes de nginx -t.
Un aviso honesto: la imagen oficial de nginx ya genera configuración desde /etc/nginx/templates con envsubst al arrancar. Para ficheros estáticos o credenciales, Compose tiene configs y secrets, que tratamos en variables de entorno y secretos en Docker Compose. Lo que añade pre_start aquí es la validación: si nginx -t falla, el contenedor del servicio ni siquiera arranca.
Qué pasa cuando un paso pre_start falla
Un paso que sale con un código distinto de 0 detiene el arranque de ese servicio y de los que dependen de él. Para verlo, rompí la configuración con un fichero de sobrescritura que ponía CACHE_MAX_AGE: '3600"', con una comilla de más. Compose recreó el contenedor porque el entorno había cambiado, ejecutó los pasos y respondió service "web" pre_start[1] exited with code 1 (hook container 0b71eb9063ca retained for inspection), con código de salida 1.
El resultado tiene tres consecuencias que conviene conocer:
- El contenedor
b3p9-web-web-1se quedó en estadoCreated, y el nginx que antes respondía 200 dejó de responder (curldevolvió000). Cualquier cambio que recree el servicio vuelve a ejecutar los pasos, así que un paso roto tumba un servicio que funcionaba - Compose conserva el contenedor del paso fallido para que lo inspecciones. No aparece en
docker compose ps -ani endocker compose logs, tiene un nombre aleatorio (naughty_buck) ydocker compose downlo elimina. Compose 5.3.0 todavía no lo conservaba - El primer paso no se deshace: el
default.confroto se quedó en el volumen
Para leer el error busca el contenedor por sus etiquetas:
docker ps -a \
--filter label=com.docker.compose.project=b3p9-web \
--filter label=com.docker.compose.hook=pre_start
docker logs 0b71eb9063ca
La última línea de docker logs fue nginx: configuration file /etc/nginx/nginx.conf test failed, precedida de nginx: [emerg] unexpected """ in /etc/nginx/conf.d/default.conf:3.
Las notas de la 5.5.1 anuncian que Compose muestra la salida de los ganchos y la incluye en el error. En mis pruebas no apareció ni en up -d, ni en up sin -d, ni con --progress plain. Repetí la prueba con Docker Engine 29.8.0 dentro de un contenedor docker:29.8.0-dind y pasó lo mismo.
La causa probable está en el propio motor: docker logs -f sobre un contenedor que todavía no ha arrancado termina al instante sin salida en ambas versiones, y Compose abre ese flujo antes de arrancar el paso. Hasta que eso cambie, docker logs sobre el contenedor conservado es la forma fiable de ver qué pasó.
Cuándo se vuelve a ejecutar un paso pre_start
La documentación dice que un paso que ya terminó bien se salta en los siguientes up si su definición no cambia. Lo que medí con Compose 5.5.1 sigue otra regla, la del código: con up, los pasos se ejecutan cuando ningún contenedor del servicio está en marcha. Lo comprobé con docker events filtrado por la etiqueta del paso y con un fichero contador en un volumen:
| Acción | ¿Se ejecuta el paso? |
|---|---|
Segundo up -d con el servicio en marcha |
No |
docker compose restart |
No |
docker compose stop y después start |
No |
docker compose stop y después up -d |
Sí |
El proceso principal muere (restart: "no") y luego up -d |
Sí |
restart: on-failure:3 reinicia el servicio 3 veces |
No (4 arranques, 1 ejecución) |
| Cambias solo un atributo del paso | Sí, y recrea el servicio |
up -d --force-recreate |
Sí |
docker compose run --rm --no-deps |
No |
| Escalar de 2 a 3 réplicas | No |
| Alternar Compose 5.3.0 y 5.5.1 en el mismo proyecto | Sí, porque recrea los contenedores |
La fila de stop seguido de up -d es la que más sorprende: el paso había terminado bien y nada había cambiado, pero volvió a ejecutarse. El último caso encaja con las notas de la 5.5.0[6], que avisan de que el primer up tras actualizar puede recrear contenedores porque cambia el cálculo de los digests de imagen. Actualizar Compose, por tanto, puede repetir tus migraciones. La conclusión práctica es escribir cada paso para que se pueda repetir sin daño: miniflux -migrate no hace nada si el esquema ya está al día, y chown -R es inofensivo aunque cueste tiempo en un volumen grande.
Cuándo usar pre_start y cuándo un servicio de un solo uso
El patrón clásico declara la tarea como un servicio aparte con restart: "no" y hace que la aplicación dependa de él con condition: service_completed_successfully. Sigue funcionando en Compose 2.40.3 y en 5.5.1, y lo probé con la misma migración de Miniflux. Este extracto omite db y las variables, que son las del ejemplo anterior:
services:
migrate:
image: miniflux/miniflux:2.3.3
command: ["/usr/bin/miniflux", "-migrate"]
restart: "no"
depends_on:
db:
condition: service_healthy
miniflux:
image: miniflux/miniflux:2.3.3
depends_on:
migrate:
condition: service_completed_successfully
Las diferencias que observé entre los dos enfoques:
| Criterio | Servicio de un solo uso | pre_start |
|---|---|---|
| Versión mínima de Compose | funciona en 2.40.3 | 5.3.0 |
docker compose ps -a |
muestra migrate-1 Exited (0) |
no muestra el paso |
Segundo up -d con todo en marcha |
vuelve a arrancar migrate (en 2.40.3) |
salta el paso |
| Compartir la tarea entre servicios | sí, dos servicios pueden depender de ella | no, el paso pertenece a un servicio |
| Volúmenes propios de la tarea | sí | no |
| Ver la salida | docker compose logs migrate |
docker logs del paso, y solo si falla |
| Servicio con réplicas | la tarea es independiente | una ejecución para todas |
Mi recomendación es usar pre_start cuando todas las máquinas que ejecutan el fichero tienen Compose 5.3.0 o posterior y la tarea pertenece a un único servicio. Quédate con el servicio de un solo uso si el fichero debe funcionar con el Compose de Debian o Ubuntu, si dos servicios comparten la tarea o si esta necesita montajes propios. Para tareas que lanzas a mano de vez en cuando, como una restauración, encaja mejor un servicio con perfiles en Docker Compose.
Límites de pre_start en Docker Compose 5.5.1
Además de los ya vistos, estos son los límites que encontré o verifiqué en el código y la documentación:
per_replica: true:docker compose config -qlo acepta, peroupcrea los contenedores y después falla conper_replica is not yet supported; remove per_replica or set it to false- Réplicas y volúmenes: con
replicas: 2el paso se ejecutó una sola vez y usó los volúmenes de la réplica 1. Un fichero que escribió en un volumen anónimo solo apareció enworker-1, y untmpfsdel servicio nunca llegó a verse desde el paso - Volúmenes propios: un paso no puede declarar montajes ni cambiar los del servicio. Un montaje
:rodel servicio dioRead-only file systemdentro del paso. La petición para cambiarlo es la incidencia #13934[7], abierta desde el 10 de julio de 2026 - Ficheros de sobrescritura: un
pre_starten un segundo fichero-fse añade a la lista del primero en lugar de sustituirla.docker compose configmostró la migración dos veces; la etiqueta!overridela reemplaza - Imágenes:
docker compose config --imageslista las imágenes de los pasos ydocker compose pulllas descarga junto a las del servicio - Coste: cada paso es un contenedor que se crea, arranca y borra. Con la máquina compartida (carga media de 38 a 50 en 18 núcleos),
up -dde un servicio Alpine tardó una mediana de 489 ms sin paso y 970 ms con un pasotrue, en 7 repeticiones de cada caso
No he probado Docker Desktop, Windows, macOS ni x86_64, ni la herencia de una imagen construida con build:, que el pull request #13862[8] describe y cubre con tests.
Preguntas frecuentes
¿Funciona pre_start con Docker Compose v2?
No. Compose 2.40.3 y 5.2.0 rechazan el fichero completo con services.web additional properties 'pre_start' not allowed (o el nombre de tu servicio) y no crean nada. Necesitas Compose 5.3.0 o posterior, disponible en el repositorio de Docker y en Docker Desktop 4.82.0 o superior.
¿Se ejecuta pre_start con docker compose run?
No. Con docker compose run --rm --no-deps miniflux /usr/bin/miniflux -version el contenedor imprimió 2.3.3 y no se creó ningún contenedor de paso. Los pasos solo se disparan cuando Compose arranca los contenedores del servicio con up.
¿Dónde veo la salida de un paso que ha fallado?
En docker logs sobre el contenedor que Compose conserva, que encuentras con docker ps -a --filter label=com.docker.compose.hook=pre_start. En mis pruebas con Compose 5.5.1 y Docker Engine 29.5.2 y 29.8.0, ni docker compose logs ni el mensaje de error incluyeron esa salida.
Conclusión
pre_start convierte la migración, el chown o la generación de configuración en parte del servicio que los necesita, sin contenedores Exited en ps ni cadenas de depends_on. A cambio exige Compose 5.3.0 en todas partes, repite los pasos en más situaciones de las que sugiere la documentación y, cuando falla tras un cambio, deja parado un servicio que funcionaba. Empieza por un paso idempotente en un servicio no crítico, provoca un fallo a propósito y localiza el contenedor conservado antes de confiarle una migración de producción.
Fuentes
- documentación de Docker sobre init containers
- especificación de Compose
- 5.0.0, publicada el 2 de diciembre de 2025
- documentación de línea de comandos
- código de pre_start.go en la 5.5.1
- notas de la 5.5.0
- incidencia #13934
- pull request #13862
- docker/compose: versión v5.3.0
- Docker Desktop: notas de versión
- Debian: paquete docker-compose en trixie
- Ubuntu: paquete docker-compose-v2 en noble-updates
- Docker Docs: Merge Compose files y la etiqueta !override
Código fuente
Accede a todo el código fuente de este artículo en GitHub.
Ver en GitHub