docker compose run creates a fresh container for a service and attaches your terminal to it. Compared to exec (Tip #34), which jumps into a running container, run is for one-shot tasks — migrations, ad-hoc scripts, REPLs, smoke tests. The flags around it decide what gets started, what gets cleaned up, and whether ports are published.

–rm — clean up after exit

By default, the container created by run stays around (stopped) after the command finishes. That accumulates dead containers fast in a dev loop. --rm removes the container as soon as it exits:

docker compose run --rm api npm test

Use it on every interactive or short-lived invocation. The only reason not to is when you need to inspect logs or filesystem state afterwards.

–service-ports — actually publish the ports

A surprise for newcomers: run does not publish the ports declared in the service definition by default. Compose avoids clashing with whatever might already be running from up.

services:
  web:
    image: myweb
    ports:
      - "8080:80"
docker compose run --rm web                # 8080 is NOT bound
docker compose run --rm --service-ports web  # 8080 IS bound

Reach for --service-ports when running the service interactively (e.g., a dev server you want to hit from your browser).

-e / –env — ad-hoc environment variables

Override or add env vars for a single invocation, without editing the file:

docker compose run --rm \
  -e LOG_LEVEL=debug \
  -e FEATURE_FLAG=experimental \
  api ./bin/run-once.sh

Useful for debugging a specific run, toggling a flag, or injecting a test secret without polluting the environment of up.

–entrypoint — override the entrypoint

run lets you replace the image’s entrypoint for the duration of the command:

# The image's entrypoint is /app/start.sh — bypass it to get a shell
docker compose run --rm --entrypoint sh api

Combined with --rm, this is the canonical “give me a shell in a freshly built image” command.

–build — rebuild before running

If the service has a build: section, --build rebuilds the image before starting the container:

docker compose run --rm --build api ./tests.sh

Equivalent to docker compose build api && docker compose run --rm api ./tests.sh, just shorter. Reach for it when you’re iterating on the Dockerfile and want each run to pick up the latest layers.

–no-deps — skip the dependency graph

By default, run service also starts every service in depends_on. For a quick command that doesn’t need them, that’s wasted setup:

# Spin up db + redis + their deps, then run
docker compose run --rm api ./check-config.py

# Just run, ignore depends_on
docker compose run --rm --no-deps api ./check-config.py

If up is already running the stack, --no-deps is also the right flag — Compose won’t try to start them again.

-v / –volume — one-off bind mount

Inject a host path into the container for a single run:

docker compose run --rm \
  -v "$(pwd)/seed.sql:/tmp/seed.sql:ro" \
  db psql -U postgres -f /tmp/seed.sql

Handy for one-time seeding, dumping data out to the host, or testing a script that lives outside the build context.

–name — pin the container name

run-created containers default to <project>-<service>-run-<hash>. Give it a memorable name when you want to attach later or reference it in logs:

docker compose run --rm --name api-debug api sh
# In another terminal: docker logs api-debug

A common combo: shell inside a built image

The flag combo you reach for most often when poking at a misbehaving build:

docker compose run --rm \
  --no-deps \
  --entrypoint sh \
  --build \
  api

Rebuilds the image, drops you into a shell, doesn’t bring up dependencies, and cleans up when you exit.

Pro tip: aliases for the long forms

The flag set quickly becomes muscle memory, but the typing isn’t. A pair of aliases pays off:

alias dcr='docker compose run --rm'
alias dcrsh='docker compose run --rm --no-deps --entrypoint sh'

Then dcrsh api opens a clean shell in the API image without spinning up the rest of the stack.

Further reading