Docker Compose Entrypoint

Written by

in

Docker Compose Entrypoint: A Complete Configuration Guide

Understanding how to configure a docker compose entrypoint correctly is essential for anyone building reliable multi-container applications. The entrypoint directive controls exactly what process runs when a container starts, and getting it wrong leads to containers that exit immediately, ignore your arguments, or fail to pass health checks. This guide walks through how entrypoints work, when to override them, and how to avoid the most common mistakes.

What the Docker Compose Entrypoint Actually Does

Every Docker image has a default entrypoint baked into it by the ENTRYPOINT instruction in its Dockerfile. When you set a docker compose entrypoint in your docker-compose.yml, you are overriding that baked-in default for a specific service, without needing to rebuild the image. This is one of the most useful features of Compose because it lets you reuse the same base image across different services with different startup behavior.

The entrypoint is the executable that always runs first inside the container. Anything you specify under command is passed to that entrypoint as arguments, not as a replacement process. This distinction trips up a lot of engineers who are new to Docker: they expect command to run standalone, but it actually gets appended to whatever the entrypoint is.

Entrypoint vs Command: The Core Distinction

The relationship between entrypoint and command is straightforward once you see it written out:

  • entrypoint defines the fixed executable that runs on container start.
  • command defines the default arguments passed to that executable.
  • If you override entrypoint in Compose without also setting command, the image’s own default CMD may still be appended, or dropped entirely, depending on how the entrypoint script handles arguments.
  • Setting entrypoint: [] clears the entrypoint entirely, which is a common debugging trick to get a plain shell instead of the image’s normal startup logic.
  • This matters most when you’re troubleshooting a container that won’t start. If a docker compose entrypoint script has a bug, the container might crash before you ever see application logs, because the failure happens before your app code even runs.

    Shell Form vs Exec Form

    Docker supports two syntaxes for entrypoint, and Compose respects both:

    services:
      app:
        image: myapp:latest
        entrypoint: ["/usr/local/bin/docker-entrypoint.sh"]
        command: ["node", "server.js"]

    The array syntax above is exec form — it runs the process directly without a shell wrapping it, which means signals like SIGTERM reach your process directly. This matters a great deal for graceful shutdowns; a shell-form entrypoint often swallows signals unless it explicitly traps and forwards them.

    Shell form looks like this instead:

    services:
      app:
        image: myapp:latest
        entrypoint: /usr/local/bin/docker-entrypoint.sh

    Both are valid, but exec form is generally preferred in production because it avoids an extra shell process and gives you cleaner signal handling.

    Overriding the Docker Compose Entrypoint for Debugging

    One of the most practical uses of a docker compose entrypoint override is debugging a container that keeps crashing before you can inspect it. Instead of letting the container run its normal startup script, you can force it into a shell:

    docker compose run --entrypoint /bin/sh app

    This drops you into a shell inside the container’s filesystem, using the same image, volumes, and environment variables the service would normally get, but without executing the problematic startup logic. From there you can manually run the original entrypoint script, step through it, and see exactly where it fails.

    Debugging a Crash Loop

    A typical crash-loop investigation looks like this:

    docker compose logs app
    docker compose run --rm --entrypoint /bin/sh app
    # inside the container:
    cat /usr/local/bin/docker-entrypoint.sh
    sh -x /usr/local/bin/docker-entrypoint.sh

    Running the script with sh -x prints every command as it executes, which quickly surfaces missing environment variables, unreachable dependencies, or permission errors that the normal container startup would otherwise hide behind a generic exit code. If you haven’t already looked at the raw output, it’s worth reviewing how to read container output in general — see this guide to debugging with Compose logs for a deeper walkthrough of log inspection.

    Common Entrypoint Script Patterns

    Most production entrypoint scripts follow a similar shape: wait for a dependency, run any one-time setup, then hand off to the main process. A minimal but realistic example:

    #!/bin/sh
    set -e
    
    echo "Waiting for database..."
    until pg_isready -h db -p 5432; do
      sleep 1
    done
    
    echo "Running migrations..."
    node ./migrate.js
    
    echo "Starting application..."
    exec "$@"

    The exec "$@" line at the end is critical. It replaces the shell process with your application process rather than spawning it as a child, which means signals sent to the container (like SIGTERM during a docker compose down) reach your application directly instead of being absorbed by the wrapper script.

    Docker Compose Entrypoint With Environment Variables

    A docker compose entrypoint script frequently needs to read environment variables to decide how to behave — which database to wait for, which config file to load, or whether to run in development or production mode. Compose passes environment variables into the container before the entrypoint runs, so your script can reference them immediately.

    services:
      api:
        image: myapi:latest
        entrypoint: ["/entrypoint.sh"]
        environment:
          - NODE_ENV=production
          - DB_HOST=db
          - DB_PORT=5432
        env_file:
          - .env

    If you’re managing a larger set of variables across services, it’s worth reading through a dedicated guide on managing Compose environment variables rather than scattering environment: blocks inconsistently across every service.

    Passing Runtime Arguments Alongside Environment Variables

    Sometimes you need both: environment variables for configuration and command-line arguments for behavior. The entrypoint script can combine both sources:

    #!/bin/sh
    set -e
    
    if [ "$NODE_ENV" = "production" ]; then
      exec node --max-old-space-size=512 server.js "$@"
    else
      exec node --inspect server.js "$@"
    fi

    This pattern lets one image serve multiple environments without maintaining separate Dockerfiles, which keeps your build pipeline simpler and reduces the number of images you need to test and patch.

    Docker Compose Entrypoint for Init Containers and One-Off Tasks

    A docker compose entrypoint isn’t only for long-running services. It’s also useful for one-off tasks like running database migrations, seeding data, or generating configuration files before the main application starts. You can define a dedicated service purely for this purpose:

    services:
      migrate:
        image: myapp:latest
        entrypoint: ["node", "./migrate.js"]
        depends_on:
          - db
    
      app:
        image: myapp:latest
        entrypoint: ["/entrypoint.sh"]
        command: ["node", "server.js"]
        depends_on:
          - migrate

    Keep in mind that depends_on only controls startup order, not readiness — it does not wait for the migration to actually finish successfully. For genuine sequencing you typically need a healthcheck or an explicit wait step inside the entrypoint script itself. If you’re setting up a database alongside this pattern, the Postgres Compose setup guide covers healthchecks in more detail.

    Restart Policies and Entrypoint Failures

    If your docker compose entrypoint script exits with a non-zero status, Compose’s restart policy determines what happens next. A restart: on-failure policy will keep retrying, which can be useful for transient failures like a database not being ready yet, but it can also mask a genuinely broken entrypoint if you’re not watching the logs.

    services:
      app:
        image: myapp:latest
        entrypoint: ["/entrypoint.sh"]
        restart: on-failure:5

    Limiting the retry count, as shown above, prevents an infinite crash loop from consuming resources indefinitely while you’re still debugging.

    Rebuilding After Entrypoint Changes

    If you change the entrypoint script inside your image rather than just the Compose override, you need to rebuild the image for the change to take effect — Compose caches image layers aggressively. This is a common source of confusion: an engineer edits docker-entrypoint.sh, restarts the container, and sees no change because the old image layer is still in use.

    docker compose up --build

    For a deeper look at when Compose actually rebuilds versus when it reuses a cached layer, see this guide on rebuilding services correctly. If you’re still deciding between putting startup logic in the Dockerfile versus overriding it in Compose, the Dockerfile vs Docker Compose comparison is worth reading first, since it explains where each tool’s responsibilities naturally end.

    Security Considerations for Entrypoint Scripts

    Because the entrypoint script runs with whatever permissions the container process has, it’s worth checking that it doesn’t run as root unnecessarily. Many official images now include a USER instruction after the entrypoint setup completes its root-level tasks (like binding to a privileged port) and before handing off to the application. If your entrypoint reads secrets from environment variables, avoid printing them in logs — a stray echo $DB_PASSWORD left in a debugging pass is an easy way to leak credentials into your log aggregation system. For handling secrets more deliberately, see this guide on secure config management.

    You can find more detail on the underlying mechanics directly from Docker’s own documentation on the Dockerfile ENTRYPOINT instruction, and the official Compose file reference for the exact fields Compose supports at the service level.

    Conclusion

    A docker compose entrypoint override is a small piece of configuration with outsized influence over how reliably your containers start, shut down, and report failures. Getting the exec-form syntax right, understanding how command interacts with entrypoint, and building a debugging habit around --entrypoint /bin/sh will save significant time when something in your stack doesn’t come up cleanly. Treat entrypoint scripts with the same care as application code — test them, keep them idempotent, and make sure they forward signals properly with exec "$@" so your containers stop as gracefully as they start.

    FAQ

    Does command in Compose override the entrypoint entirely?
    No. command supplies arguments to whatever entrypoint is set to; it does not replace the entrypoint process itself. To bypass the entrypoint entirely, you need to override entrypoint directly, for example with docker compose run --entrypoint.

    Why does my container exit immediately after I set a custom entrypoint?
    This usually happens when the entrypoint script doesn’t end with exec "$@" or an equivalent long-running command, so the shell simply finishes and the container exits. Check that the last line of your script actually starts a persistent process.

    Can I set the docker compose entrypoint to an empty value?
    Yes. Setting entrypoint: [] (or an empty string in older Compose syntax) clears any entrypoint from the image, which is often used together with a manual command for debugging or running an alternate process inside the same image.

    Is there a difference between entrypoint behavior in Compose and plain docker run?
    No, the underlying mechanics are identical since Compose is a orchestration layer over the same Docker Engine API. The --entrypoint flag on docker run behaves the same way as the entrypoint key in a Compose file. For the full command reference, see Docker’s CLI documentation.

    Comments

    Leave a Reply

    Your email address will not be published. Required fields are marked *