In the past, I've used the commands docker compose exec [some container] bash and docker compose run [some container] bash pretty much interchangeably. I figured there was some subtle, probably unimportant, difference between them. Recently I learned what it is.

Recently I had a problem where I ran the run variant to check the contents of a configuration file and to my surprise it was missing entirely! I suspected this exec/run business to be the culprit so I tried using exec instead and sure enough the missing configuration file was there.

As it turns out, docker compose exec [some container] [command] will run the given command in the docker container that is already running. On the other hand, docker compose run [some container] [command] spins up a fresh container.

But how does that explain the missing configuration file? In our case we have a Dockerfile that's laid out kind of like this:

FROM ubuntu:latest as base

WORKDIR /app
# ... Do a bunch of things ...

FROM base as development

# ... Do a bunch of extra things ...
# Build all the configuration files:
CMD ./entrypoint.sh

Then we have a docker-compose.yml that looks like this:

# ...
services:
    app:
        build:
            context: .
            target: development
# ...

With this setup, when I ran docker compose up, it would build the container with the development target and the configuration files would be created. Then when I ran docker compose exec app bash, I'd see the files. However when I ran docker compose run app bash, it would make a new container that was built with the base target which didn't have the files!

If you're like me, even after learning this you'll have some trouble recalling which command is which, so here's a silly mnemonic to help:

I'm still not exactly clear why the run command doesn't just build the new container with the development target but I'll update this TIL if I figure it out.