deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

Docker Compose pre_start steps replace the migrate-and-seed pseudo-service pattern

Compose 5.3's pre_start hook runs setup containers to completion before a service starts, ending the fake-service depends_on pattern — with predictable re-runs and one gap: successful step output vanishes.

Docker Compose pre_start steps replace the migrate-and-seed pseudo-service pattern

Compose gains init containers

Docker Compose 5.3 shipped a pre_start option in July, and Docker Desktop has included it since then, according to a hands-on review published on dev.to by Remdore. The feature lets a service declare setup containers — init containers, in effect — that run to completion, in order, before the service itself starts. The author tested it against a Postgres 18 container and a small Node app and concluded it replaces a workaround that most real-world Compose files have been carrying for years.

The workaround it kills

As the dev.to post describes it, every production-ish Compose file tends to contain two services that are not really services: one that runs database migrations and one that seeds data. They get chained to the application through depends_on conditions such as service_completed_successfully, because Compose previously had no way to express "run this before that."

The pattern works but leaks. Each start leaves exited containers behind, and a second docker compose up -d re-runs the migrate and seed jobs, because from Compose's perspective they are merely stopped services that should be running again. The author notes that idempotent seed scripts — the usual defence against this — are themselves a workaround people stop noticing they have built.

How pre_start works

With the new option, migration and seed steps are declared directly on the service that needs them. Each step runs as its own container, sequentially, and the service starts only after the final step exits with code 0. A step can specify its own image, which is how a migration can run psql from the Postgres image while the application uses a Node image; a step without an image inherits the service's. Steps also share the service's network, environment and mounts, so a bind mount defined on the app is visible inside a step running a different image, and depends_on conditions are honoured before the first step runs.

The step containers are anonymous and short-lived: the author observed them via docker events, created and destroyed within roughly a second, with randomly generated names. After up, docker compose ps -a shows only the application and database containers.

Timed cold starts with --wait came in at 3.9 seconds without steps, 5.2 seconds with two pre_start steps, and 6.7 seconds for the old two-service pattern — about 0.65 seconds per step, and still faster than the approach it replaces.

When steps re-run

The rerun semantics are the part the author cared about most. Per the review, steps re-run when the service container is recreated, when the step definition changes, or when the previous run failed. A plain up on an already-running stack triggers nothing, and neither does docker compose restart. Scaling the service to three replicas ran the steps once, not three times — execution is per-service, not per-replica.

The specification includes a per_replica: true flag for the per-replica case, but Compose 5.5 currently accepts it in configuration and then rejects it at up time with an explicit error — and that error arrives after the database has already come up healthy, not when the file is parsed.

One storage caveat: because step containers disappear immediately, anything written to a tmpfs or an anonymous volume is lost. Steps should write to named volumes or bind mounts, or not at all.

What happens on failure

When the author forced a seed step to exit with code 3, up -d returned 1, the application stayed in Created and never started, and a third service depending on the application also remained in Created. The failed step's container is retained for inspection, with the project label attached, and docker logs against it shows the step's output. docker compose down removes it, and the next up runs the step again rather than assuming it had passed.

The one real gap

When a step succeeds, its output goes nowhere — not in the up output, not when attached, not with --progress plain or --verbose, and not in docker compose logs, because the container that produced it was deleted seconds after exit. Output from a migration tool reporting how many migrations were applied is simply gone unless something fails. The author's workaround was to have steps write a marker row to a table, and they expect the gap to be fixed eventually, since briefly retaining successful hook containers should be a small change. Until then, it is the reason half of their migrations remain on the old pattern.

Why it matters

pre_start formalises something nearly every non-trivial Compose setup has been faking with pseudo-services, and it removes the three costs of that fakery: leftover exited containers, accidental re-runs on subsequent up commands, and idempotency workarounds in seed scripts. The re-run rules are predictable, failures block dependent services cleanly, and the feature brings Compose closer to the init-container semantics Kubernetes users already have. The vanished output of successful steps is the one caveat worth knowing before migrating critical migration jobs to it. Compose 5.3 or newer is required; docker compose version will confirm availability.

  • #docker
  • #docker-compose
  • #containers
  • #devops
  • #orchestration

Related posts