---
title: "04 — Docker Basics: Image, Container, Dockerfile"
uid: docker-basics
tags: ["dockerfile", "roadmap:docker", "docker", "containers", "cli", "images", "fundamentals"]
excerpt: "Dockerfile is the recipe, image is the frozen snapshot you can ship, container is one running serving. Three words, one pipeline."
date: 2026-08-13T03:28:15+0000
source: https://www.aveshina.my.id/en/blog/docker-basics
---

I kept losing the thread between *Dockerfile*, *image*, and *container* until I lined them up as one pipeline. The model that clicked is almost a cooking metaphor: **a Dockerfile is the recipe, an image is the frozen dish you can ship, a container is one serving of that dish on a plate right now.** [1][4]

The three are a strict pipeline — each one produces the next — and that ordering is what unstuck me. A Dockerfile is *built* into an image; an image is *run* into a container. You can't run a Dockerfile and you can't build a container, and once that sounds obvious, the rest of Docker's command surface falls into place [1].

## The three-part pipeline

```figure
<svg viewBox="0 0 720 240" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="A left-to-right pipeline with three boxes connected by arrows. Box 1: Dockerfile, a text page with FROM, RUN, COPY, CMD lines. Arrow labelled docker build. Box 2: Image, a stack of read-only layers. Arrow labelled docker run. Box 3: Container, a running process inside a writable layer, marked live.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- 1 Dockerfile -->
    <rect x="30" y="60" width="180" height="130" rx="8" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="120" y="82" font-size="13" font-weight="700" fill="#422006" text-anchor="middle">Dockerfile</text>
    <g font-family="ui-monospace, monospace" font-size="10.5" fill="#422006">
      <text x="48" y="106">FROM node:20</text>
      <text x="48" y="124">WORKDIR /app</text>
      <text x="48" y="142">COPY . .</text>
      <text x="48" y="160">RUN npm ci</text>
      <text x="48" y="178">CMD ["node", "server.js"]</text>
    </g>
    <text x="120" y="208" font-size="10" font-style="italic" fill="#64748b" text-anchor="middle">text recipe</text>

    <!-- arrow build -->
    <path d="M214,120 L268,120" fill="none" stroke="#64748b" stroke-width="1.6" marker-end="url(#barr)"/>
    <defs><marker id="barr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#64748b"/></marker></defs>
    <text x="241" y="112" font-size="10" font-weight="700" fill="#475569" text-anchor="middle">docker build</text>

    <!-- 2 Image -->
    <rect x="270" y="60" width="180" height="130" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="360" y="82" font-size="13" font-weight="700" fill="#1e1b4b" text-anchor="middle">Image</text>
    <g>
      <rect x="292" y="96"  width="136" height="16" rx="3" fill="#c7d2fe" stroke="#6366f1" stroke-width="1"/>
      <rect x="292" y="116" width="136" height="16" rx="3" fill="#c7d2fe" stroke="#6366f1" stroke-width="1"/>
      <rect x="292" y="136" width="136" height="16" rx="3" fill="#c7d2fe" stroke="#6366f1" stroke-width="1"/>
      <rect x="292" y="156" width="136" height="16" rx="3" fill="#c7d2fe" stroke="#6366f1" stroke-width="1"/>
    </g>
    <text x="360" y="208" font-size="10" font-style="italic" fill="#64748b" text-anchor="middle">frozen read-only layers</text>

    <!-- arrow run -->
    <path d="M454,120 L508,120" fill="none" stroke="#64748b" stroke-width="1.6" marker-end="url(#barr)"/>
    <text x="481" y="112" font-size="10" font-weight="700" fill="#475569" text-anchor="middle">docker run</text>

    <!-- 3 Container -->
    <rect x="510" y="60" width="180" height="130" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="600" y="82" font-size="13" font-weight="700" fill="#052e16" text-anchor="middle">Container</text>
    <rect x="528" y="96" width="144" height="18" rx="3" fill="#bbf7d0" stroke="#16a34a" stroke-width="1.1" stroke-dasharray="3 2"/>
    <text x="600" y="109" font-size="9.5" fill="#052e16" text-anchor="middle">+ writable layer</text>
    <rect x="528" y="118" width="144" height="14" rx="3" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
    <rect x="528" y="134" width="144" height="14" rx="3" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
    <rect x="528" y="150" width="144" height="14" rx="3" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
    <rect x="528" y="166" width="144" height="14" rx="3" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
    <circle cx="600" cy="78" r="4" fill="#22c55e"/>
    <text x="600" y="208" font-size="10" font-style="italic" fill="#64748b" text-anchor="middle">live process</text>
  </g>
</svg>
```

Each artifact has one job, and the command that produces it is named after the destination, not the source — docker build makes an *image*, docker run makes a *container* [1][4].

## The Dockerfile: a recipe of layer-producing instructions

A **Dockerfile** is a plain text file of instructions, each one producing a new layer in the resulting image [2]. A handful of instructions cover most real Dockerfiles:

```
FROM node:20-alpine            # base image — the bottom layer
WORKDIR /app                   # set the working directory for later steps
COPY package*.json ./          # copy host files into the image
RUN npm ci                     # run a build command (creates a layer)
COPY . .                       # copy the rest of the source
EXPOSE 3000                    # document which port the app listens on
CMD ["node", "server.js"]      # the process to start when the container runs
```

Two instructions I had to learn to tell apart: RUN executes at *build* time (it bakes results into the image), while CMD is what the container *runs* at startup [2]. Conflating them is the classic beginner mistake — putting npm start in a RUN will build fine and then do nothing at runtime because there's no CMD.

## The image: a frozen, layered snapshot

A **Docker image** is the read-only, layered artifact the build produces — application code, runtime, libraries, and config, all stacked [4]. It's immutable; once built, it doesn't change, which is what makes it safe to ship. The same image run on my laptop, in CI, and in production is, by construction, the same artifact [4].

Images are addressed by name and tag: node:20-alpine, my-app:1.2.0, postgres:16. The tag is just a label — :latest is the default when I omit one, and it's the most misleading label in Docker, because "latest" doesn't mean newest-built, it means whatever was last pushed under that name [4]. I treat :latest as a footgun in any non-trivial workflow.

## The container: a running instance

A **container** is a live process — an image plus a writable scratch layer on top, plus a running instance of the CMD [1]. The key behaviors that follow from that definition:

- One image can produce many containers. docker run my-app five times gives five independent processes.
- Writes inside the container land in the writable layer and are *gone* when the container is removed. Persistence needs volumes, covered in a later post.
- Stopping a container (docker stop) freezes the process; the writable layer survives until I docker rm the container. Removing the container is what discards its scratch state [1].

## The command surface, in pipeline order

The handful of commands I use daily maps cleanly onto the pipeline. Memorize them in build → run → manage order and the surface stops feeling sprawling [1]:

```
# images
docker pull nginx              # fetch an image from a registry
docker build -t my-app:1.0 .   # build an image from the Dockerfile in .
docker image ls                # list local images
docker image rm my-app:1.0     # delete an image

# containers
docker run -d -p 8080:3000 --name api my-app:1.0   # run a container
docker container ls                                 # list running containers
docker container ls -a                              # include stopped ones
docker container stop api                           # stop a container
docker container rm api                             # remove a stopped container
```

The -d flag means detached (background), -p host:container maps a host port to a container port, and --name gives the container a stable name I can use in later commands instead of copying a random hash [1]. That's roughly 80% of day-to-day Docker.

## How I use this

The vocabulary pipeline is the lens I use to read error messages and plan changes. When docker run complains image not found, I know I either forgot to docker build first or mistyped the tag — the source artifact doesn't exist. When I need to change app behavior, I edit the Dockerfile and rebuild, because the image is immutable and patching a running container's files is a lie that disappears on the next redeploy. And when a container keeps restarting, I reach for docker logs api to read what the CMD's process is printing before it dies. Naming the artifact in the pipeline — recipe, snapshot, instance — points at the right fix almost every time.

## References

[1] Docker, Inc., "Docker concepts — Running containers," Docker Docs, 2024. [Online]. Available: [https://docs.docker.com/get-started/docker-concepts/running-containers/](https://docs.docker.com/get-started/docker-concepts/running-containers/)

[2] Docker, Inc., "Dockerfile reference," Docker Docs, 2024. [Online]. Available: [https://docs.docker.com/engine/reference/builder/](https://docs.docker.com/engine/reference/builder/)

[3] Docker, Inc., "Dockerfile best practices," Docker Docs, 2024. [Online]. Available: [https://docs.docker.com/develop/develop-images/dockerfile_best-practices/](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/)

[4] Amazon Web Services, "What's the difference between Docker images and containers?," aws.amazon.com, 2024. [Online]. Available: [https://aws.amazon.com/compare/the-difference-between-docker-images-and-containers/](https://aws.amazon.com/compare/the-difference-between-docker-images-and-containers/)

```quiz
Q: Which sequence correctly produces a running container from source?
- docker run Dockerfile → docker build container
- docker build Dockerfile → image → docker run image → container
- docker pull container → docker start image
correct: 1
explain: A Dockerfile is built into an image, and an image is run into a container. You cannot run a Dockerfile or build a container directly.

Q: What is the difference between RUN and CMD in a Dockerfile?
- RUN executes at build time; CMD is the process started when the container runs
- They are interchangeable aliases
- CMD executes at build time; RUN starts at container runtime
correct: 0
explain: RUN bakes results into a layer during the build. CMD specifies the default process the container will execute when it starts.

Q: What happens to files written inside a container's writable layer when the container is removed?
- They are committed back into the image automatically
- They are discarded — the writable layer only lives as long as the container
correct: 1
explain: The writable layer is per-container and ephemeral. Removing the container deletes it. Persistence requires a volume or bind mount.

Q: Why is the `:latest` tag considered misleading?
- It always points to the newest image you have built locally
- It is just a label meaning "whatever was last pushed under that name," with no guarantee of recency or version
correct: 1
explain: `:latest` is the default tag, not a guarantee of freshness. Two teams can both push `my-app:latest` and override each other, so production should pin explicit version tags.

Q: `docker run -d -p 8080:3000 my-app` does what?
- Runs the image in the foreground and prints port 8080 to the console
- Starts the container detached (background) and maps host port 8080 to container port 3000
correct: 1
explain: `-d` detaches the container into the background, and `-p host:container` forwards host port 8080 to the container's port 3000.
```
