skip to content

Docker Compose

Declaring a multi-container stack in one compose.yaml: services, networks, volumes, environment and startup order, so a whole local environment comes up with one command. Interviewers probe it because depends_on orders startup but never waits for readiness.

on this pageshow

explore

questions

16

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

In Docker Compose, what is the difference between the project `.env` file and a service's `env_file` and `environment` attributes?

level: juniorimportance: must knowfreq 66%

basics

~20 s

The project .env file only feeds ${VAR} substitution while Compose parses compose.yaml. A service's env_file and environment attributes set variables inside its container; nothing in .env reaches a container unless one of them passes it on.

open as a page

What problem does Docker Compose solve, and what are the main top-level sections of a compose.yaml file?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Compose runs a multi-container stack from one declarative YAML file instead of many long docker run commands. The main top-level keys are services (the containers), networks, and volumes. docker compose up creates everything; down removes it.

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

In a compose.yaml, how do `${VAR:-default}`, `${VAR-default}` and `${VAR:?err}` differ, and when would you use each one?

level: middleimportance: must knowfreq 52%

basics

~20 s

${VAR:-default} uses the default when VAR is unset or empty, ${VAR-default} only when it is unset, and ${VAR:?err} makes Compose stop with err when VAR is unset or empty. Use defaults for tunables and :? for values that must be supplied.

open as a page

How do containers in a Docker Compose project reach each other over the network, and what is the difference between the `ports` and `expose` keys in a compose file?

level: middleimportance: must knowfreq 60%

basics

~20 s

Compose puts every service on a project network where the service name resolves via Docker's embedded DNS, so the app connects to db:5432 using the container-side port. ports publishes a container port to the host; expose publishes nothing — it is documentation only.

open as a page

In a Docker Compose file, does the `depends_on` key guarantee that a dependency such as a database is ready to accept connections before the dependent service starts? How do you make startup ordering actually reliable?

level: middleimportance: must knowfreq 65%

basics

~20 s

No. Plain depends_on only orders container start, not readiness — the database process may still be initialising. Add a healthcheck to the dependency and use depends_on: <svc>: condition: service_healthy, and still make the app retry its connection.

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

How do you use `docker compose config` to trace where a service's wrong variable value came from, and what do `--environment`, `--variables` and `--no-interpolate` add?

level: middleimportance: should knowfreq 42%

basics

~20 s

docker compose config prints the merged, interpolated model, with env_file contents folded into each service's environment. --no-interpolate shows which placeholder feeds a value, --environment lists the variables Compose resolved, and --variables lists every placeholder with its default.

open as a page

How does a Docker Compose stack keep a database's data across `docker compose down` and a later `up`, and what is the difference between a named volume and a bind mount declared in a compose file?

level: middleimportance: should knowfreq 55%

basics

~20 s

Declare a named volume under the top-level volumes: and mount it at the data path. Named volumes are Docker-managed, survive down, and are only deleted by down -v. Bind mounts map a host path into the container — great for source code, but host-permission and performance sensitive.

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

With Docker Compose, if LOG_LEVEL is set by `docker compose run -e`, the service's `environment`, its `env_file` and the image's `ENV`, which value does the container process see?

level: seniorimportance: should knowfreq 31%

basics

~20 s

docker compose run -e wins, then the service's environment, then env_file, and the image's ENV only applies when Compose sets nothing for that name. Any ${VAR} inside environment or env_file is interpolated first, from the shell or env files.

open as a page

With Docker Compose, a stack renders one image tag on your laptop and another in CI: which sources feed compose.yaml interpolation, and in what order do they win?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Variables exported in the shell win first. Next come files passed with --env-file, later ones overriding earlier ones. Only when no --env-file is given does Compose read .env from the project directory, which defaults to the first compose file's folder.

open as a page

How would you run the same Docker Compose stack with different settings for local development and for an automated integration-test run, without duplicating the whole file — and how do the `.env` file, the `environment:` key and shell variables interact?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Keep one base file and layer overrides: compose.override.yaml is merged automatically, or pass -f base.yaml -f ci.yaml explicitly. Use profiles to switch optional services on. .env supplies values for ${VAR} interpolation in the file; environment:/env_file: set variables inside the container.

open as a page

A team proposes running their production deployment as a Docker Compose stack on a single VM, updated by pulling new images and re-running `docker compose up -d`. What are the tradeoffs, and when would you argue for a different approach?

level: principalimportance: nice to knowfreq 35%

basics

~20 s

It is defensible for low-stakes, single-node workloads: simple, cheap, one reviewable file. But it gives no multi-node scheduling, no self-healing beyond restart policies, and up -d recreates containers, so updates mean downtime. Push back when uptime, scale or blast radius matter.

open as a page