Docker Compose pre_start: When to Use a Hook or a Migration Job
Use Docker Compose pre_start for a service startup check, give shared migrations one owner, and rehearse what happens when either step fails.
DM
Suppose a release adds a column to an orders database. Your API reads it and your worker writes it. Both services use Docker Compose, and both have a good reason to refuse startup until the column exists. It’s tempting to put the migration in each service’s startup hook. Now two services can try to change the same database, while neither owns the release.
Use Docker Compose pre_start for a task that belongs to a service’s startup, such as checking that its required schema exists. Give a shared migration one owner and an explicit success check. If both services need to wait for a migration in the same Compose project, make it a named one-shot service and declare it as a dependency. Across projects or hosts, coordinate it through your release process.
What a pre_start hook actually promises
The Compose service reference documents pre_start from Compose 5.3.0. Compose runs each step in a temporary container before starting the main service. An unsuccessful exit blocks that service and its dependents. Compose 5.7.0, released on 2 November 2026, combined creation and startup into a single plan for ordinary up flows. That change is a reason to review how your deployment uses up; it didn’t introduce the hook or turn it into an application health check.
A hook that already succeeded for its current definition can be skipped on a later up. Recreating the service, changing the step definition or recovering from a failed step can make it run again. A container restart under its restart policy doesn’t rerun the hook. That leaves two cases to plan for: a check you need on every application start belongs in the application’s own startup path, and a command that changes shared state needs to tolerate retries.
Give the check and the change different owners
In the example release, one migration job creates the orders column. The API’s hook only checks whether the schema it needs is present, using an account that can read the necessary metadata. The worker can perform its own compatibility check without trying to apply the same change.
This fragment assumes an existing Compose file with api and db, a meaningful database health check, and an API image whose entrypoint can run the example command. /app/bin/check-schema is your script, not a Compose command. Have it exit nonzero if it cannot connect or finds an incompatible schema. Keep credentials out of the example file and out of the logs.
services:
api:
depends_on:
db:
condition: service_healthy
pre_start:
- command: ["/app/bin/check-schema", "orders-v2"]The database’s health check and the hook answer different questions. A probe that only checks connections tells you whether the server is accepting them; it doesn’t prove the schema fits this app version. Docker’s startup order guide also documents service_completed_successfully, which is the dependency condition for a separate migration service that must finish before the API and worker start. A successful exit should mean the migration tool verified the intended database state, not merely that a process ran.
If two projects can reach the same database, a project dependency doesn’t serialize releases between them. Likewise, setting per_replica: false scopes a hook to its service rather than each replica; it isn’t a lock across services, projects or hosts. Use one deployment owner and the migration tool’s coordination mechanism where concurrent attempts are possible. A failed release can leave earlier changes committed, so plan compatible schema changes and recovery separately from the start gate.
Rehearse both outcomes
Use a disposable Compose project and a separate database. A project name separates Compose-managed resources, but it won’t make an external database or shared bind mount private. Check those targets before running anything. In the rehearsal directory, verify the version and resolved config with the Compose config command, then bring up the test project.
docker compose version
docker compose -p orders-rehearsal config --quiet
docker compose -p orders-rehearsal up -d
docker compose -p orders-rehearsal ps -a- Failure case: start with a fixture that lacks the expected schema. Verify the hook fails and the main API process does not start.
- Success case: apply the migration once to the fixture database, rerun the project, and verify the API and worker can process a synthetic order.
- Repeat case: try a second unchanged up and a normal container restart in the rehearsal. Check whether the application remains safe even when the hook doesn’t run again.
If a hook fails, inspect its retained container before retrying. The up reference documents the hook label and notes that the next up can remove that evidence. List candidate containers with docker ps -a --filter label=com.docker.compose.hook=pre_start, confirm the ID belongs to the rehearsal project, then read its logs with docker logs. If you need a retention policy for ordinary service logs, see Krova’s Docker log rotation guide.
Write down three things before closing the release: which job owns the schema change, what blocks a bad startup, and what the app does when it restarts later. If one answer is “Compose will run the migration again,” test that assumption before you depend on it.
Rehearse the Compose release on a separate Cube
Run your own Docker and Compose stack in a self-managed Linux environment. Keep the rehearsal database and credentials separate, and verify the same deployment commands you plan to use in production.
Creation context: This AI-assisted guide uses a hypothetical release and documented Compose behavior. The example is illustrative; no Docker workload or migration was run for this article. The cover is a generated illustration.




