skip to content

Compose CLI & Dev Loop

Driving a stack with up, down, ps, logs, exec and build, knowing that run creates a fresh container while exec enters a running one, and that watch syncs source without a full rebuild. Interviewers watch you do this live on a screen share.

on this pageshow

explore

questions

5

With Docker Compose, what is the difference between `docker compose run` and `docker compose exec`, and when do you use each?

level: juniorimportance: must knowfreq 64%

answer

  1. new container versus existing one
  2. one of them needs a running service
  3. one-off jobs leave containers behind
  4. --rm and --no-deps belong to run

basics

~20 s

docker 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
bash
# 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 -a

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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.
open as a page

With Docker Compose, you edit the API's source and run `docker compose restart api`, but the old code still runs. Why, and what fixes it?

level: middleimportance: must knowfreq 56%

basics

~20 s

The code is baked into the image, and docker compose restart only restarts the existing container from that old image. Rebuild and recreate with docker compose up -d --build api; a plain up skips the build because a local image already exists.

open as a page

With a Docker Compose stack running detached, how do you see which services are up and follow only the API's recent log lines?

level: juniorimportance: should knowfreq 47%

basics

~20 s

Run docker compose ps to list the project's running containers, adding -a to include exited ones, then docker compose logs -f --tail 50 api to print the api service's last 50 lines and keep streaming new ones.

open as a page

In Docker Compose, how do `-p` (a second stack copy) and `up --scale` (more replicas) differ, and what collides in each?

level: middleimportance: should knowfreq 28%

basics

~20 s

-p starts a separate project whose containers, network and named volumes all carry the new name; --scale adds replicas of one service inside the same project. Both still collide on fixed host ports and on a fixed container_name.

open as a page

With Docker Compose, when would you use `docker compose watch` with `sync` instead of a bind mount or `rebuild`, and what are its limits?

level: seniorimportance: should knowfreq 33%

basics

~20 s

Use watch sync when edited source should reach a running container quickly but selectively: it copies changed files one way into the container and honours ignore rules. Use rebuild for dependency or Dockerfile changes. Container-side writes never come back.

open as a page