SSD Nodes Learn 🎉 VPS from $5.50/mo
How to do am Matt ConnorBy Matt Connor

docker compose exec: enter running container shell

Use docker compose exec to enter a running service shell; use run --rm when service don stop or you no wan disturb the existing container.

Get interactive shell with docker compose exec

docker compose exec web bash dey open interactive shell inside the container wey already dey run as the web service. The name after exec na the service name from your compose.yaml, no be the container name. If the image no get bash, request sh instead.

docker compose ps
docker compose exec web bash

Run docker compose ps first. E suppose list web with state running. Then the second command go put you for prompt inside the container, and exit or Ctrl-D go carry you back to the host. The service go continue to run after you comot, because exec start another process beside the main one. Closing your shell no affect PID 1 (process id 1), wey be the process wey dem build the container to run.

That one na one of the two ways to enter. exec joins container wey already dey exist. docker compose run creates new container from the same service definition. Almost everything else for this guide dey follow from that one difference.

Why -it optional for Compose but required with plain docker

Flags two dey control interactive part of session. -i dey keep stdin open, so wetin you type go reach the process. -t dey allocate pseudo terminal, wey dem dey call TTY, so shell fit print prompt and handle arrow keys. Plain docker exec leave both of dem off by default. Na why every example wey you don see dey write docker exec -it. docker compose exec dey turn both on for you, so docker compose exec -it web bash and docker compose exec web bash dey do the same thing. Compose still dey accept -it so old muscle memory fit continue to work.

You go notice missing TTY within seconds. Shell go run, but e no go print prompt and Ctrl-C no go reach the process. The opposite case, where you need tell Compose make e no allocate TTY, get its own flag and its own section further down.

Wetin to do when the image no get bash

Ask an Alpine based image for bash and exec fail like this:

OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH: unknown

That message no be exec problem. E mean say the binary wey you ask for no dey inside the image. Alpine dey ship BusyBox, wey provide ash as /bin/sh, and no bash at all, so ask for sh:

docker compose exec web sh

Debian and Ubuntu based images, including the -slim tags, get bash, and bash give you command history plus better completion. So try bash first, then fall back to sh. sh dey for almost every general purpose image.

Some images no get shell at all. Distroless images and images wey build FROM scratch get only the application binary and the libraries wey e need, intentionally, because shell wey no dey there no fit dey used against you. For those images, sh fail with the same message, and nothing remain to try. Two approaches dey work. Google's distroless images publish :debug tags wey add BusyBox shell, so you fit switch the tag temporarily to enter. Or start a separate container inside the target's namespaces:

CID=$(docker compose ps -q web)
docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot

You don now get netshoot tools wey dey point to the application's network, so curl localhost:8080 and ss -lntp behave as if you dey inside the application. The filesystem wey you see belong to netshoot, not the app. Because dem share the process namespace, ls /proc/1/root/ fit reach the target's own files when you be root.

When service no dey run, use docker compose run --rm

exec need container wey dey run. If you point am to service wey stop, e go reject am:

service "web" is not running

E no go start anything for you. docker compose run go:

docker compose run --rm web bash

run go create new container from the web service definition, with the same image, environment, volumes and networks, then e go replace the service command with the one wey you type. --rm go delete that container when you exit. If you leave --rm out, the leftover containers go gather under names like myproject-web-run-4f1c2b. docker compose ps -a go show dem, but nothing else go clean dem up.

Two behaviours of run dey surprise people. E no publish the service ports unless you add --service-ports, and na deliberate: another container wey bind host port 8080 while the first one still dey hold am go fail with bind: address already in use. E also start everything wey the service list under depends_on before your shell show, so quick inspection fit start database and cache. --no-deps go skip that.

run pass through the image ENTRYPOINT, but exec no dey do that. exec start your command directly inside the existing container, so the entrypoint script no go see am. Under run, your bash go reach that script as arguments. Plenty official images end their entrypoint with exec "$@", so e go pass through directly and you go get your shell. Script wey dey interpret its own arguments fit do another thing with dem. For that one run, you fit replace the entrypoint:

docker compose run --rm --entrypoint sh web

Na this be the most common reason why command wey work under exec go behave differently under run. The difference between command and entrypoint explain which half of the image configuration you dey replace each time.

exec or run: how you go choose

  • exec need make container dey run. run no need am, and run fit start dependencies.
  • exec dey see live process list and files as dem dey now, including anything wey application don write since e start. run dey get clean copy of the image, so none of those things dey inside.
  • exec dey skip entrypoint. run dey run am.
  • run dey leave container behind unless you pass --rm.

Use exec to check wetin dey really happen. Use run --rm for temporary copy of the same environment, for one-time migration command, or when the real service no go stay up long enough for you to exec inside am.

Useful exec flags: user, working directory and replicas

Most images dey switch go non-root user, so if you try install diagnostic tool inside your exec shell, e go stop for here:

E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)

-u root dey give you root shell for the same container:

docker compose exec -u root web sh

-w /srv/app dey set the working directory for only that command. -e KEY=value dey add environment variable to your session, no be to the service. When service dey run more than one replica, --index 2 dey choose which container you go enter. If na file ownership for mounted directory you dey investigate, PUID and PGID for container images explain why na numeric ids, no be user names, dey decide who fit write there.

Get psql or mysql shell inside database container

Client dey already inside database image, so you no need install one for host, and you no need publish the port:

docker compose exec db psql -U postgres -d app
docker compose exec db mariadb -u root -p

Postgres images carry psql, MySQL images carry mysql, and MariaDB images carry mariadb. Connection dey happen from inside container, so this one go work even when compose file no publish any database port. Na the safer arrangement be that: nothing for internet fit reach port wey you never publish.

One trap fit waste person afternoon. Your shell dey expand variables for host before Docker see the command, so -U "$POSTGRES_USER" go send empty string when that variable dey only inside container. Single quotes and shell inside container go expand am for the correct place:

docker compose exec db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"'

No use docker compose run --rm db here without command. E go start second Postgres server against the same data volume, and e go refuse to start:

FATAL:  lock file "postmaster.pid" already exists

The lock file dey do the work wey e suppose do, because two servers wey dey write one data directory fit corrupt am. While database dey up, exec into the container wey dey run. Whether database suppose dey for Compose at all na separate decision, and running database for Docker or for host explain the trade-off.

Services wey need console when startup: stdin_open and tty

exec and run cover shells wey you open by hand. Service wey its main process naturally interactive need two keys for the compose file:

services:
  console:
    image: python:3.12-slim
    command: python
    stdin_open: true
    tty: true

stdin_open: true na docker run -i and tty: true na docker run -t. Without dem, container go start and exit immediately with code 0, and docker compose ps -a go show Exited (0). Nothing crash. python wey no get terminal for stdin go read end of file immediately and quit normally. Na the correct behaviour for program wey nobody dey type into.

When you set both keys, attach to the process wey dey run:

docker attach $(docker compose ps -q console)

Detach with Ctrl-P then Ctrl-Q. This one go leave the process dey run. This sequence only dey work when container get both TTY and stdin open. Ctrl-C instead go send interrupt to PID 1 and stop the service.

Leave both keys off for ordinary services. Web server no dey read stdin, and tty: true dey make many programs switch to colour output and line buffering because dem believe say person dey watch. This one go fill docker compose logs with escape codes.

Why scripted exec dey break for cron and CI: the -T flag

An exec command wey dey work for your terminal fit fail inside cron job or continuous integration (CI) runner:

the input device is not a TTY

Compose dey ask for pseudo terminal by default, but cron no give the job any terminal. So the request go fail before your command even run. -T dey turn off this request:

0 3 * * * docker compose -f /srv/app/compose.yaml exec -T db pg_dump -U postgres -Fc app > /srv/backups/app.dump

-T important for another reason. TTY dey rewrite the byte stream as e dey comot, so compressed dump wey pass through am go arrive damaged. Any redirected or piped output need -T.

Two more cron details. Pass -f with absolute path, because cron dey run the job from home directory where compose file no dey. Compose go then stop with no configuration file provided: not found. Also, exec dey return the exit code of the command wey e run. So if pg_dump fail, e go make your script fail under set -e instead of writing empty backup and reporting success. You fit find the other everyday commands for Compose command cheat sheet wey good to keep near those scripts.

Why changes wey you make inside container dey disappear

You install tool with exec, edit config file, fix the problem, and one week later the fix don disappear. Na the container writable layer dey behave as e suppose. docker compose up -d after any change to the image tag or service definition destroy the old container and build new one from the image, and every hand edit go disappear together with the old container.

docker compose restart different. E stop and start the same container, so hand edits remain. Na why manual fix fit look like say e go last for weeks, then disappear during unrelated update. Named volumes and bind mounts survive both operations, because their data dey outside the container, and bind mounts and named volumes explain which one you suppose choose for data wey you intend to keep.

So treat exec shell as place to read and test. Once you know the fix, write am where e go survive: put package inside Dockerfile, and put setting inside compose file. Then docker compose up -d to apply am, and confirm with another exec say the new container really get am.

FAQ

Wetin be the difference between docker compose exec and docker compose run?

exec dey run command inside container wey already dey run, beside the main process, and e dey skip the image entrypoint. run dey create new container from the same service definition with the same image, environment, volumes and networks, pass your command through the entrypoint, and start any depends_on services first. run still leave the service ports unpublished unless you add --service-ports. Use exec to inspect the live service. Use run --rm when the service stop or when you no want disturb am.

Why docker compose exec dey report say the service no dey run?

exec dey attach to existing container and e no fit create one, so stopped or crashed service go give service "web" is not running. Check docker compose ps -a, wey dey list exited containers with status like Exited (1), and read docker compose logs web to know why e stop. To get shell anyway, run docker compose run --rm --entrypoint sh web. That one build fresh container from the same service definition without allowing the broken start command to run.

How I fit open shell when the image no get bash?

docker compose exec web bash wey fail with exec: "bash": executable file not found in $PATH mean say bash no dey inside the image, and this normal for anything wey build on Alpine. Use docker compose exec web sh, because BusyBox dey provide /bin/sh. Distroless and scratch images no get shell at all, so no exec command go work. Switch to the image :debug tag if the publisher get one, or start debug container for the target namespaces with docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot, where $CID come from docker compose ps -q web.

Why my exec command dey fail with "the input device is not a TTY" inside cron?

docker compose exec dey request pseudo terminal by default, but cron no dey provide one, so the request fail before your command run. Add -T to switch am off: docker compose exec -T db pg_dump -U postgres app. Use -T for any redirected or piped output too, because TTY dey change the byte stream and fit damage binary dump. For cron, also pass -f with the absolute path to your compose file, or Compose go exit with no configuration file provided: not found.

The changes wey I make inside container with exec go survive restart?

Dem survive docker compose restart, wey dey reuse the same container. Dem go lost for docker compose up -d after any image or configuration change, because that one recreate the container from the image and discard its writable layer. Data wey you write into named volumes or bind mounts survive both, because e dey outside the container. Make diagnostic changes with exec, then put the permanent version inside the Dockerfile or compose file.