Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions content/manuals/compose/compose-sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@ keywords: docker compose sdk, compose api, Docker developer SDK
title: Using the Compose SDK
linkTitle: Compose SDK
weight: 60
params:
sidebar:
badge:
color: green
text: New
---

{{< summary-bar feature_name="Compose SDK" >}}
Expand Down
173 changes: 173 additions & 0 deletions content/manuals/compose/how-tos/init-containers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
---
title: Use init containers in Compose
linkTitle: Use init containers
weight: 120
description: Use pre_start init containers to run setup tasks before a service starts in Docker Compose.
keywords: docker compose init containers, pre_start, docker compose migrations, volume permissions, docker compose lifecycle
params:
sidebar:
badge:
color: green
text: New
---

{{< summary-bar feature_name="Compose pre_start" >}}

Init containers are short-lived containers that run before a service's main container starts. They execute sequentially, each running to completion before the next begins. If any step exits with a non-zero code, the service will not start.

Use them for setup work that must finish before the application boots: running database migrations, fixing volume permissions, generating dynamic configuration, or executing any ordered sequence of prerequisites.

Compose models init containers as [`pre_start`](/reference/compose-file/services.md#pre_start) lifecycle hooks. Unlike [`post_start`](/reference/compose-file/services.md#post_start) and [`pre_stop`](/reference/compose-file/services.md#pre_stop), which run a command inside the running service container, each `pre_start` step runs in its own ephemeral container created after the service container is created but before it is started.

## When not to use init containers

For static files and secrets, use the native [`configs`](/reference/compose-file/configs.md) and [`secrets`](/reference/compose-file/secrets.md) top-level elements instead. Compose mounts them directly into containers with a configurable target path, mode, UID, and GID. No init container required.

For background tasks with their own lifecycle - scheduled backups, post-exit cleanup, periodic maintenance — init containers are the wrong tool. Those tasks run independently of service startup, not before it.

## How `pre_start` containers run

Each step in a service's `pre_start` list:

- Runs in its own ephemeral container, created after the service container is created but before it starts.
- Inherits the service's image by default. Set `image` to override.
- Joins the same networks as the service, so it can reach services declared in [`depends_on`](/reference/compose-file/services.md#depends_on).
- Shares the service's volume mounts, so files written to a shared volume are immediately visible to the service.
- Must exit `0` for the next step, and the service itself, to start. A non-zero exit aborts startup for the service and anything that depends on it.

A `pre_start` step is skipped on subsequent `docker compose up` runs if it previously succeeded, its definition hasn't changed, or when the service container restarts under its `restart` policy. It reruns if the definition changes, the previous run failed, or the service is recreated with `--force-recreate`.

## Examples

### Run a database migration before the app starts

In the following example, `app` waits for `db` to be healthy, then runs
`./manage.py migrate` in an ephemeral container that reuses the app's
image. The service container only starts once the migration exits `0`.

```yaml
services:
app:
image: myapp:latest
depends_on:
db:
condition: service_healthy
pre_start:
- command: ["./manage.py", "migrate"]

db:
image: postgres:18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two problems with this db service that would prevent it from actually working:

  1. No POSTGRES_PASSWORD — Postgres refuses to start without it (or POSTGRES_HOST_AUTH_METHOD=trust), with the error: Error: Database is uninitialized and superuser password is not specified. You must specify POSTGRES_PASSWORD to a non-empty value for the superuser.

  2. Version inconsistency — this example uses postgres:18 but the services.md reference example uses postgres:16. Pick one version and use it consistently across both files.

At minimum the db service should look like:

  db:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: example
      POSTGRES_USER: myuser
      POSTGRES_DB: mydb
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      retries: 5
      start_period: 30s
      timeout: 10s

healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
retries: 5
start_period: 30s
timeout: 10s
```

If the migration fails, `app` does not start and the failure is reported in
the `docker compose up` output.

### Fix volume ownership before a non-root service starts

Named volumes are created with root ownership. When the service runs as a
non-root user, you can use a `pre_start` step to adjust ownership before
the service mounts the volume.

```yaml
services:
app:
image: myapp:latest
user: "1000:1000"
volumes:
- data:/data
pre_start:
- image: busybox
user: root
command: sh -c 'chown -R 1000:1000 /data'

volumes:
data:
```

The `pre_start` step uses a different image (`busybox`) and runs as `root`,
even though the service itself runs as user `1000`.

### Chain multiple setup steps

`pre_start` steps run in declared order. The next step only starts once the
previous one exits `0`. In the following example, the application waits
for migrations to finish, then for seed data to load, before starting.

```yaml
services:
app:
image: myapp:latest
depends_on:
db:
condition: service_healthy
pre_start:
- command: ["./manage.py", "migrate"]
- command: ["./manage.py", "loaddata", "fixtures.json"]

db:
image: postgres:18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue here: postgres:18 with no POSTGRES_PASSWORD — this db container won't start. Same version inconsistency vs services.md (postgres:16).

```

Each step runs in its own ephemeral container. If the second step fails,
the first step is not rolled back, but `app` does not start.

### Replace the one-shot service pattern

Before `pre_start`, the common way to express "run X before Y starts" was
to model the setup work as a service with `restart: "no"` and have the main
service `depends_on` it with `condition: service_completed_successfully`:

```yaml
services:
migrate:
image: myapp:latest
command: ["./manage.py", "migrate"]
restart: "no"

app:
image: myapp:latest
depends_on:
migrate:
condition: service_completed_successfully
```

The equivalent expressed with `pre_start`:

```yaml
services:
app:
image: myapp:latest
pre_start:
- command: ["./manage.py", "migrate"]
```

`pre_start` is preferable because:

- The setup work is modeled as a subordinate step of the service, not as a peer service that exits immediately.
- Completed steps do not appear as exited services in `docker compose ps`.
- Chaining several setup steps does not require a web of `depends_on` edges between one-shot services.
- The ephemeral container inherits the service's image by default, so no duplicate `image:` declaration is needed.

The one-shot service pattern still has its place when the setup work is a shared concern that multiple services depend on, or when it needs to be addressable independently of any single service.

## Limitations

- `pre_start` runs once for the service as a whole, not once per replica (`per_replica: false`). Per-replica execution (`per_replica: true`) is not yet supported.
- Volume mounts shared across replicas (named volumes, bind mounts) are accessible from a `pre_start` step. Per-instance mounts such as `tmpfs`
or anonymous volumes cannot be addressed by a single shared run.
- `pre_start` does not re-trigger when you scale a service up. A step runs again only on definition change, prior failure, or `--force-recreate`.

## Reference and additional information

- [`pre_start`](/reference/compose-file/services.md#pre_start)
- [`post_start`](/reference/compose-file/services.md#post_start)
- [`pre_stop`](/reference/compose-file/services.md#pre_stop)
- [`depends_on`](/reference/compose-file/services.md#depends_on)
- [Use lifecycle hooks](/manuals/compose/how-tos/lifecycle.md)
- [Control startup order](/manuals/compose/how-tos/startup-order.md)
6 changes: 3 additions & 3 deletions content/manuals/compose/how-tos/lifecycle.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
title: Using lifecycle hooks with Compose
linkTitle: Use lifecycle hooks
weight: 20
description: Learn how to use Docker Compose lifecycle hooks like post_start and pre_stop to customize container behavior.
keywords: docker compose lifecycle hooks, post_start, pre_stop, docker compose entrypoint, docker container stop hooks, compose hook commands
weight: 110
description: Learn how to use Docker Compose lifecycle hooks like pre_start, post_start, and pre_stop to customize container behavior.
keywords: docker compose lifecycle hooks, post_start, pre_stop, pre_start, docker compose entrypoint, docker container stop hooks, compose hook commands
---

{{< summary-bar feature_name="Compose lifecycle hooks" >}}
Expand Down
2 changes: 1 addition & 1 deletion content/manuals/compose/how-tos/provider-services.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Use provider services
description: Learn how to use provider services in Docker Compose to integrate external capabilities into your applications
keywords: compose, docker compose, provider, services, platform capabilities, integration, model runner, ai
weight: 112
weight: 130
---

{{< summary-bar feature_name="Compose provider services" >}}
Expand Down
41 changes: 41 additions & 0 deletions content/reference/compose-file/services.md
Original file line number Diff line number Diff line change
Expand Up @@ -1752,6 +1752,47 @@ services:

For more information, see [Use lifecycle hooks](/manuals/compose/how-tos/lifecycle.md).

### pre_start

{{< summary-bar feature_name="Compose pre_start" >}}

`pre_start` defines a sequence of init containers to run before the service container is started. Each step runs to completion, in declared order, and the service container only starts once every step has exited `0`. A non-zero exit fails the bring-up of the service and its dependents.

Unlike `post_start` and `pre_stop`, which run a command inside the running service container, each `pre_start` step runs in its own ephemeral container, created after the service container is created but before it is started. Possible values are:

- `command`: The command to run. Optional when the chosen image's entrypoint already runs the intended command.
- `image`: The image used for the ephemeral container. If omitted, the parent service's image is used.
- `user`: The user to run the command. If not set, defaults to the user declared in `image` (or to the main service command's user when `image` is omitted).
- `privileged`: Lets the `pre_start` command run with privileged access.
- `working_dir`: The working directory in which to run the command. If not set, it is run in the same working directory as the main service command.
- `environment`: Sets the environment variables to run the `pre_start` command. The command inherits the `environment` set for the service's main command, and this section lets you append or override values.
- `per_replica: false`: Whether the hook runs once for the service as a whole before any replica starts.
Comment thread
aevesdocker marked this conversation as resolved.

A `pre_start` container joins the same networks as the service, so it can reach services declared in `depends_on`, and shares the service's declared volume mounts so files it produces in a shared volume are visible to the service. With `per_replica: false` and a scaled service, only mounts that are shared across replicas (named volumes, bind mounts) are usable. Per-instance mounts (`tmpfs`, anonymous volumes) cannot be addressed by a single run.

A `pre_start` step that has already succeeded for its current definition is not re-run on a subsequent `up`, nor when the service container restarts under its `restart` policy. A step runs again when its definition changes, when the previous run did not succeed, or when the service is recreated.

```yaml
services:
app:
image: myapp:latest
depends_on:
db:
condition: service_healthy
pre_start:
- command: ["./manage.py", "migrate"]
- image: busybox
command: sh -c 'chown -R 1000:1000 /data'
volumes:
- data:/data

db:
image: postgres:16
Comment thread
aevesdocker marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example is confusing because it mixes two unrelated concerns in one pre_start sequence:

  1. ./manage.py migrate — a DB migration (needs the db service)
  2. chown -R 1000:1000 /data — fixing ownership of a volume

The app service has volumes: data:/data but the db service has no volume. A reader trying to understand pre_start from this example will wonder why the app is chowning a path that the database doesn't share, and why the database has no persistent volume of its own.

Also, db: image: postgres:16 has no POSTGRES_PASSWORD, POSTGRES_USER, or POSTGRES_DB — the postgres container won't start.

Suggest splitting into two focused examples: one for migrations (the DB case, already done in init-containers.md) and one for volume ownership (the busybox/chown case, already done separately in init-containers.md). The combined example here adds confusion rather than showing a realistic use case.


volumes:
data:
```

### `pre_stop`

{{< summary-bar feature_name="Compose pre stop" >}}
Expand Down
2 changes: 2 additions & 0 deletions data/summary.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ Compose label file:
requires: Docker Compose [2.32.2](https://github.com/docker/compose/releases/tag/v2.32.2) and later
Compose lifecycle hooks:
requires: Docker Compose [2.30.0](https://github.com/docker/compose/releases/tag/v2.30.0) and later
Compose pre_start:
requires: Docker Compose [5.3.0](https://github.com/docker/compose/releases/tag/v5.3.0) and later
Compose mac address:
requires: Docker Compose [2.23.2](https://github.com/docker/compose/releases/tag/v2.23.2) and later
Compose menu:
Expand Down