With Docker Compose, what is the difference between `docker compose run` and `docker compose exec`, and when do you use each?
answer
- new container versus existing one
- one of them needs a running service
- one-off jobs leave containers behind
- --rm and --no-deps belong to run
basics
~20 sdocker compose run starts a new one-off container from a service's definition; docker compose exec runs a command inside that service's already running container. Use run for migrations and scripts, exec to inspect a live service.
solid answer
~40 s`docker compose run api ./bin/migrate` creates a **fresh** container from the `api` service's definition (same image, volumes, networks and environment) with your command replacing the service's `command`. It starts the services `api` depends on unless you pass `--no-deps`, does not publish the service's `ports` unless you add `--service-ports` or `--publish`, and keeps the stopped container unless you pass `--rm`. `docker compose exec db psql -U app` instead starts a process in the container that `up` already started for `db`; it fails if that service is not running, and it sees the live container's filesystem and environment. So one-off jobs such as migrations, seeders or a test run go through `run --rm`, and inspecting a live service or opening a database shell goes through `exec`.
code
bash · 11 lines# one-off migration in a fresh api container; db is already up, so skip deps; remove afterwards
docker compose run --rm --no-deps api ./bin/migrate
# SQL shell inside the Postgres container that `up -d` started
docker compose exec db psql -U app -d shop
# feed a file to the running database from a script (no TTY for piped input)
docker compose exec -T db psql -U app -d shop < seed.sql
# leftover one-off containers are hidden from plain ps
docker compose ps -ago deeper
Recall the one-line split: run makes a new container from the service definition, exec enters the container already running. Know that run keeps its container unless you add --rm.
Explain what run changes compared with up: it replaces the command, skips the service's ports, starts dependencies unless --no-deps and drops the restart policy. Know that exec needs a running container.
Show judgment about side effects: migrations through run --rm --no-deps against a database that is already up, exec for inspection only, and never hand-editing a live container instead of rebuilding.
Frame run as the team convention for one-off tasks: idempotent migration and seed commands invoked the same way locally and in CI, so nobody depends on state left inside a container.
## Two commands, two lifecycles Docker Compose offers two ways to get a command running "inside" a service, and they differ in the one thing that matters most: **which container** the command runs in. - **`docker compose run SERVICE CMD`** creates a **new, one-off container** from the service's definition in `compose.yaml` and runs `CMD` in it. - **`docker compose exec SERVICE CMD`** runs `CMD` as an extra process inside the container that `docker compose up` **already started** for that service. Everything else follows from that split. A `run` container starts from the image's filesystem, so it sees nothing the live container has written to its own writable layer; only volumes are shared. An `exec` process sees the live container's filesystem, environment and network identity exactly as they are right now. ## What `docker compose run` does When you type `docker compose run api ./bin/migrate`, Compose: 1. Starts the services `api` depends on, unless you pass `--no-deps`. 2. Builds or pulls the `api` image if it is not present locally (`--build` forces a build). 3. Creates a container named `<project>-api-run-<id>` with the service's image, volumes, networks and environment. 4. Replaces the service's `command` with `./bin/migrate`. 5. Leaves out the service's `ports`, so the one-off container does not fight the running `api` for host ports. Add `--service-ports` (`-P`) to publish them, or `--publish` (`-p`) for a specific mapping. 6. Drops the service's restart policy, so a finished job is not restarted. 7. Keeps the stopped container after the command exits, unless you passed `--rm`. Other useful `run` flags are `-e KEY=value` for an extra variable, `-d` to run in the background, `-u` for a user, `-w` for a working directory and `--entrypoint` to override the image's entrypoint. ## What `docker compose exec` does `docker compose exec db psql -U app -d shop` finds the running container of the `db` service and starts `psql` in it. It creates no container, so there is nothing to clean up and it has no `--rm` flag. It fails when the service has no running container. Its flags are `-T` (no pseudo-TTY; by default one is allocated when your terminal is one), `-u`, `-w`, `-e`, `-d`, `--privileged`, and `--index N` to pick replica number N when the service runs several containers. ## Side by side | | `docker compose run` | `docker compose exec` | |---|---|---| | Container | a new one-off container | the service's running container | | Service must already run | no | yes | | Starts dependencies | yes, unless `--no-deps` | no | | Publishes the service's `ports` | only with `--service-ports` or `--publish` | not applicable | | Cleanup | `--rm`, or the container stays | nothing to clean up | | Typical use | migrations, seeders, test runs, a throwaway shell | inspecting a live service, a database shell | ## A day on an API + Postgres + Redis stack - **Morning:** `docker compose up -d` brings up `api`, `db` and `redis`. - **A new migration lands:** `docker compose run --rm --no-deps api ./bin/migrate`. `--no-deps` is safe because `db` is already running, and `--rm` leaves no container behind. - **The cache looks wrong:** `docker compose exec redis redis-cli INFO keyspace` reads the live Redis without touching it. - **Seeding from a script:** `docker compose exec -T db psql -U app -d shop < seed.sql`, where `-T` stops Compose from allocating a TTY for piped input. - **Debugging with the app's ports:** `docker compose run --rm --service-ports api sh`, but only after stopping the `api` container, because both would want the same host ports. ## Traps interviewers listen for - **Expecting `run` to see the live container's state.** It starts from the image and shares only volumes. - **Forgetting `--rm`.** One-off containers pile up, and plain `docker compose ps` hides them; `docker compose ps -a` lists them. - **Treating `exec` as a deployment tool.** A file hand-edited inside a running container is lost the next time Compose recreates it. - **Reaching for `exec` when the service is down.** `exec` never starts anything; use `up -d` first, or `run` for a one-off. - **Confusing the two `-p` flags.** On `run`, `-p` is `--publish`; the project-name `-p` is a global flag written before the subcommand. The short rule to say out loud: **`run` hires a new container for one job, `exec` walks into the one already working.**
- With the stack up, why does `docker compose run --service-ports api sh` fail with a port conflict?`--service-ports` publishes the service's declared `ports` on the one-off container, and the running `api` container already holds those host ports, so the second bind fails. That collision is exactly why plain `docker compose run` skips the service's ports by default. Stop `api` first, or publish a different mapping with `--publish`.
- You ran `docker compose run api ./bin/seed` a dozen times without `--rm`. Where are those containers, and how do you see them?Each run left a stopped container named like `<project>-api-run-<id>`. Plain `docker compose ps` excludes one-off containers, so they look absent; `docker compose ps -a` lists them. Add `--rm` to one-off commands so the container is removed when the command exits.
- How do you use `docker compose exec` from a script that pipes a file into the command?Pass `-T` so Compose does not allocate a pseudo-TTY: `docker compose exec -T db psql -U app -d shop < seed.sql`. A TTY is for interactive terminals, and piped input needs a plain stdin stream. Recent releases skip the TTY when output is not a terminal, but stating `-T` in scripts makes the intent explicit.
docker compose run is hiring a temp worker from the job description for one task; docker compose exec is walking over to the colleague already at their desk and asking them to do something now.
saying these in an interview costs you the question
- docker compose run attaches to the service container that up already started.
- docker compose exec starts the service first if its container is stopped.
- docker compose run publishes the service's ports exactly as up does.
- Adding --rm to docker compose exec removes the container afterwards.
- docker compose run --no-deps stops the services the target depends on.