---
title: "08 — Running Containers: docker run and Docker Compose"
uid: running-containers
tags: ["roadmap:docker", "docker", "docker-compose", "docker-run", "bind-mounts", "orchestration"]
excerpt: "`docker run` is one container with a pile of flags; Compose is that same pile written down once as YAML — one command starts the whole app."
date: 2026-08-13T03:28:14+0000
source: https://www.aveshina.my.id/en/blog/running-containers
---

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

```figure
<svg viewBox="0 0 720 260" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Two equivalent ways to start the same containers. Left: a stack of three docker run commands, each a long line of flags. Right: a single docker-compose.yml file feeding three connected service boxes (web, api, db) via one arrow labeled docker compose up. A bracket connects the two halves labeled same operation, two scales.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- LEFT: docker run flags -->
    <text x="150" y="22" font-size="12" font-weight="700" fill="#7f1d1d" text-anchor="middle">docker run — typed each time</text>
    <g font-family="ui-monospace, monospace" font-size="9" fill="#7f1d1d">
      <rect x="30" y="40" width="240" height="44" rx="5" fill="#fee2e2" stroke="#dc2626" stroke-width="1.1"/>
      <text x="40" y="58">docker run -d --name web \</text>
      <text x="40" y="72">  -p 8080:3000 -v ./:/app web:1.0</text>
      <rect x="30" y="92" width="240" height="44" rx="5" fill="#fee2e2" stroke="#dc2626" stroke-width="1.1"/>
      <text x="40" y="110">docker run -d --name api \</text>
      <text x="40" y="124">  -e DB=… --network appnet api:1.0</text>
      <rect x="30" y="144" width="240" height="44" rx="5" fill="#fee2e2" stroke="#dc2626" stroke-width="1.1"/>
      <text x="40" y="162">docker run -d --name db \</text>
      <text x="40" y="176">  -v pgdata:/var/lib/… postgres:16</text>
    </g>
    <text x="150" y="210" font-size="10" font-style="italic" fill="#64748b" text-anchor="middle">three long commands, never typed twice alike</text>

    <!-- arrow -->
    <defs><marker id="carr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#16a34a"/></marker></defs>
    <path d="M285,115 L360,115" fill="none" stroke="#16a34a" stroke-width="1.8" marker-end="url(#carr)"/>
    <text x="322" y="106" font-size="9.5" font-weight="700" fill="#052e16" text-anchor="middle">write it down</text>

    <!-- RIGHT: compose -->
    <text x="540" y="22" font-size="12" font-weight="700" fill="#052e16" text-anchor="middle">Compose — declared once</text>
    <rect x="370" y="40" width="180" height="34" rx="6" fill="#dcfce7" stroke="#16a34a" stroke-width="1.4"/>
    <text x="460" y="61" font-family="ui-monospace, monospace" font-size="10" font-weight="700" fill="#052e16" text-anchor="middle">docker-compose.yml</text>

    <path d="M460,76 L460,96" fill="none" stroke="#16a34a" stroke-width="1.4" marker-end="url(#carr)"/>
    <text x="490" y="90" font-size="9.5" font-weight="700" fill="#052e16">docker compose up</text>

    <g>
      <rect x="370" y="100" width="56" height="40" rx="5" fill="#bbf7d0" stroke="#16a34a" stroke-width="1.2"/>
      <text x="398" y="124" font-size="10" font-weight="700" fill="#052e16" text-anchor="middle">web</text>
      <rect x="432" y="100" width="56" height="40" rx="5" fill="#bbf7d0" stroke="#16a34a" stroke-width="1.2"/>
      <text x="460" y="124" font-size="10" font-weight="700" fill="#052e16" text-anchor="middle">api</text>
      <rect x="494" y="100" width="56" height="40" rx="5" fill="#bbf7d0" stroke="#16a34a" stroke-width="1.2"/>
      <text x="522" y="124" font-size="10" font-weight="700" fill="#052e16" text-anchor="middle">db</text>
      <line x1="426" y1="120" x2="432" y2="120" stroke="#16a34a" stroke-width="1.2"/>
      <line x1="488" y1="120" x2="494" y2="120" stroke="#16a34a" stroke-width="1.2"/>
    </g>
    <text x="460" y="166" font-size="10" font-style="italic" fill="#64748b" text-anchor="middle">one file, three services, one command</text>
    <text x="460" y="184" font-size="10" fill="#64748b" text-anchor="middle">flags → YAML keys</text>
  </g>
</svg>
```

## 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]:

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/](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/](https://docs.docker.com/engine/reference/commandline/exec/)

[3] Docker, Inc., "Docker Compose overview," Docker Docs, 2024. [Online]. Available: [https://docs.docker.com/compose/](https://docs.docker.com/compose/)

[4] Docker, Inc., "Bind mounts," Docker Docs, 2024. [Online]. Available: [https://docs.docker.com/storage/bind-mounts/](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](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](https://github.com/docker/awesome-compose)

```quiz
Q: What two operations does `docker run` combine into one command?
- pull and push
- create (make a container from an image) and start (run it)
correct: 1
explain: docker run = docker create + docker start. It instantiates a container from the image and then starts its process in one step.

Q: In `-v "$PWD:/app"`, the left side is a host path. This makes the mount a…
- named volume
- bind mount
correct: 1
explain: When the left operand of -v is a path, it's a bind mount (a host directory wired directly in). When it's a bare name, it's a Docker-managed volume.

Q: What is the main advantage of Docker Compose over a stack of `docker run` commands?
- Compose images are smaller
- The flags are written down once as declarative YAML, so a multi-service app starts repeatably with one command
correct: 1
explain: Every docker run flag has a YAML equivalent. Compose captures them in one checked-in file, which makes multi-container setups repeatable and shareable.

Q: The `-it` flag pair is used to…
- run a container in detached mode
- attach an interactive tty, typically to get a shell inside the container
correct: 1
explain: -i keeps stdin open and -t allocates a pseudo-tty. Together they let you drop into an interactive shell, either via docker run -it or docker exec -it on a running container.

Q: The bind-mount dev loop (mount source, run nodemon/vite inside) exists to…
- avoid rebuilding the image on every code change
- reduce image size
correct: 0
explain: Bind-mounting source means host edits are visible inside the container instantly, and a hot-reload tool restarts/reloads the process — no image rebuild needed per change.
```
