Run multiple startup commands with Docker Compose

Invoke a shell explicitly, preserve exit status and signals, and move complex initialization into a testable script.

Best practices
Illustration of server operations and troubleshooting

Compose's command overrides the image's default command. It does not automatically evaluate shell operators such as && or expand shell variables. Invoke a shell explicitly when you need that behavior, and inspect any existing image entrypoint before overriding startup settings.

Chain commands deliberately

For a Django image containing Python, Gunicorn, and the project files:

services:
  app:
    build: .
    command:
      - /bin/sh
      - -c
      - >-
        python manage.py migrate &&
        python manage.py collectstatic --noinput &&
        exec gunicorn --bind 0.0.0.0:8000 app.wsgi:application

Replace the WSGI module with the actual project module. && prevents startup after an earlier failure. A semicolon runs the next command regardless of the previous status. Gunicorn is used here instead of Django's development server for a deployment-style example.

The final exec replaces the shell with the service process. It improves signal delivery, but the service still needs correct shutdown handling. Do not background the final server with & and let the container's main process exit.

Move complex initialization into a script

#!/bin/sh
set -eu
python manage.py migrate
python manage.py collectstatic --noinput
exec "$@"

Package the script at /app/entrypoint.sh with executable permissions and Unix line endings, then configure:

services:
  app:
    build: .
    entrypoint: ["/app/entrypoint.sh"]
    command: ["gunicorn", "--bind", "0.0.0.0:8000", "app.wsgi:application"]

The wrapper initializes and then runs the supplied command. set -e has shell-specific conditional and pipeline exceptions, so it is not a substitute for checking complex control flow. Overriding an image entrypoint can skip that image's own required initialization; preserve or replace it knowingly.

Handle readiness and migration ownership

A migration needs a usable database, not merely a started database container. Use an appropriate health check with a service_healthy dependency or bounded application-level connection retries.

With several application replicas, running the same migration concurrently on every startup may be unsafe. A dedicated migration service with service_completed_successfully, or a deployment migration job, can make ownership explicit. The application still needs reconnection behavior for failures after startup.

FAQ

Should YAML use > or |?

> folds ordinary line breaks into spaces; | preserves them. Both can be valid when the resulting shell script is intentional. A literal block is often convenient for several separate script lines; a folded block works well for one chained command.

How do variables reach the container shell?

Compose interpolates $VAR while reading the YAML. Use $$VAR when you want a literal dollar sign to reach the container for later shell expansion. A list-form command without a shell does not expand it. Check the resolved configuration without exposing secrets.

What about several long-running processes?

Separate services are often easier to manage. If one container must host several processes, use a deliberate supervision and shutdown design rather than a chain that assumes only the last process matters.

Preserve initialization failures before container replacement. Guance log collection can retain service, container, timestamp, and error context once configured and verified with a known failure.

See Compose command and startup ordering.

Get a tailored plan

Contact us

Join the community

Scan with WeChat
to join the community

Try Guance

Start online and pay only for what you use.

Get started

Choose a Guance plan

Code hosting