08 — Running Containers: docker run and Docker Compose
A wall of docker run flags was how I started containers, and I lost the thread of what each flag did every single time. The collapse that fixed it: docker run is one container with a pile of flags; Docker Compose is that same pile written down once as YAML, so a whole multi-service app starts with docker compose up. [1][3]
The framing worth holding onto is that docker run and Compose are not competing tools — they're the same operation at two scales. Every flag on docker run has a YAML equivalent, and once I learned the mapping, Compose stopped feeling like a separate thing to learn. It's just the declarative version of a command I already understood [1][3].
docker run: one container, many flags
docker run is two operations in one: it creates a container from an image and starts it [1]. Almost everything I do day-to-day is one image plus a handful of recurring flags:
docker run -d \
--name api \
-p 8080:3000 \
-e DATABASE_URL=postgres://db:5432/app \
-v "$PWD:/app" \
--network appnet \
--restart unless-stopped \
my-app:1.2.0Each flag maps to one runtime concern [1][2]:
Flag | What it does |
|---|---|
-d | detached — run in the background, return the shell |
-p 8080:3000 | map host port 8080 to container port 3000 |
-e KEY=value | set an environment variable |
-v src:dst | mount a volume or host path (bind mount when src is a path) |
--network appnet | attach the container to a named network |
--restart unless-stopped | restart policy — bring it back after crashes/reboots |
--name api | give it a stable name for later commands |
-it | interactive + tty — for shell access into a container |
The flags I had to keep straight are the two mount flavors. -v pgdata:/var/lib/postgresql/data (a bare name on the left) is a volume; -v "$PWD:/app" (a path on the left) is a bind mount [4]. Same flag, different shape of the left operand. Confusing them is a classic source of "why is my code on the host changing but the container doesn't see it" confusion.
Interactive containers
The -it pair is the one I reach for when I need to look inside a container. -i keeps stdin open, -t allocates a pseudo-tty, and together they give me a shell inside the container's namespaces:
docker run -it --rm alpine sh # spin up a throwaway shell
docker exec -it api sh # shell into an already-running containerThe exec form is the one I use in production debugging — drop into the live container, ls around, check env vars, tail a log — without disturbing the running process [1].
Compose: write the flags down once
The moment I have more than one container, the docker run flags become unmaintainable. Three containers with ports, env vars, volumes, and a shared network means three long shell commands I'll never type the same way twice. Docker Compose replaces that pile with one declarative YAML file [3]:
services:
web:
image: my-app:1.2.0
ports:
- "8080:3000"
environment:
DATABASE_URL: postgres://db:5432/app
volumes:
- ./:/app # bind mount for live reload
depends_on:
- db
restart: unless-stopped
db:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: secret
volumes:
pgdata:Every key in that file is a docker run flag I'd otherwise be typing. ports is -p, environment is -e, volumes is -v, restart is --restart. The whole app — two services, a shared volume, a dependency order — comes up with one command [3]:
docker compose up -d # start everything, detached
docker compose logs -f web # tail one service's logs
docker compose down # stop and remove everythingThe payoff is repeatability. The YAML file is checked into git, so a new teammate runs the same three-container app with the same env vars and the same volume wiring on their first day, no README archaeology required.
The bind-mount loop for development
One pattern worth pinning down because I use it daily: the bind-mount dev loop. For local development I want the container running my app, but I want to edit the source on my host and see changes instantly — without rebuilding the image every time. The pattern is a bind mount of the source directory plus whatever hot-reload tool my runtime provides (nodemon for Node, --reload for FastAPI, Vite's HMR for frontends) [5]:
services:
web:
image: node:20
working_dir: /app
volumes:
- ./:/app
command: npx nodemon server.js
ports:
- "3000:3000"I edit server.js in my editor; the bind mount means the change is visible inside the container instantly; nodemon sees the file change and restarts the process. No rebuild. That loop is what makes developing inside containers feel as fast as developing on the host.
When to use which
The decision is purely about how many containers and how often I run them:
- One container, run it occasionally, by hand → docker run. Faster to type than authoring a YAML file.
- Anything with two or more containers, or anything I run more than a handful of times → Compose. The YAML pays for itself the second time I run docker compose up.
- Anything a teammate will also run → Compose, always. The file is the documentation.
How I use this
The habit I built is to reach for Compose the moment a project has a database. A docker-compose.yml next to my source becomes the canonical "how to run this app" — one file, one command, same result on every machine. I keep docker run for ad-hoc one-shots: spinning up a scratch container to test a command, shelling into a running service with exec -it, or running a CLI utility image with --rm. The mental test is whether I'll want to run this exact setup again — if yes, it goes in the Compose file; if it's a one-off probe, it stays on the command line.
References
[1] Docker, Inc., "docker run reference," Docker Docs, 2024. [Online]. Available: https://docs.docker.com/engine/reference/commandline/run/
[2] Docker, Inc., "docker exec," Docker Docs, 2024. [Online]. Available: https://docs.docker.com/engine/reference/commandline/exec/
[3] Docker, Inc., "Docker Compose overview," Docker Docs, 2024. [Online]. Available: https://docs.docker.com/compose/
[4] Docker, Inc., "Bind mounts," Docker Docs, 2024. [Online]. Available: https://docs.docker.com/storage/bind-mounts/
[5] DevOps Directive, "Hot reloading — Docker," courses.devopsdirective.com, 2023. [Online]. Available: https://courses.devopsdirective.com/docker-beginner-to-pro/lessons/11-development-workflow/01-hot-reloading
[6] Docker, Inc., "Awesome Compose — sample apps," GitHub, 2024. [Online]. Available: https://github.com/docker/awesome-compose
Knowledge check · Question 1 of 5
What two operations does `docker run` combine into one command?
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!