AV
HomeAboutProjectBlog

© 2026 Ave syah Shina. All rights reserved.

  1. Home
  2. Blog
  3. 08 — Running Containers: docker run and Docker Compose

08 — Running Containers: docker run and Docker Compose

August 13, 20267 min read
Download as Markdown

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 — typed each time docker run -d --name web \ -p 8080:3000 -v ./:/app web:1.0 docker run -d --name api \ -e DB=… --network appnet api:1.0 docker run -d --name db \ -v pgdata:/var/lib/… postgres:16 three long commands, never typed twice alike write it down Compose — declared once docker-compose.yml docker compose up web api db one file, three services, one command flags → YAML keys

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.0

Each 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 container

The 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 everything

The 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!