on this page
- The one mental model that fixes this
- A Dockerfile is instructions, not a thing that runs
- An image is the built result, frozen
- A container is a running instance
- The two verbs that connect everything
- Stopping a container is not deleting it
- The comparison table to keep on your desk
- Classic traps
- Why this matters when you actually ship something
You ran docker build, then docker run, then you edited a file inside the container to fix something, then you stopped it, started it back up, and your fix was gone. Now you’re staring at the terminal wondering whether Docker is broken, whether you’re broken, or both.
Neither. You just tripped over the single most common Docker confusion there is: the difference between a Dockerfile, an image, and a container. They sound like three names for the same thing. They are three completely different things, and almost every weird Docker moment a beginner hits comes from quietly mixing them up.
This guide untangles those three. If you’re fuzzy on what a container even is at the most basic level, go read Docker without the buzzwords first, since it covers the “it works on my machine” problem these tools exist to solve. This one zooms in on the three nouns and the two verbs that connect them, because once that clicks, the rest of Docker stops feeling like guesswork.
The one mental model that fixes this
Here’s the whole thing in one breath, and then we’ll slow down.
A Dockerfile is a written recipe. An image is the finished dish, cooked once and frozen solid. A container is the plate of that dish you actually sit down and eat, and when you make a mess on the plate, you’re not changing the freezer.
If you’ve written any object-oriented code, the other version lands harder: the Dockerfile is the source file, the image is the class, and a container is an object you instantiate from that class. One class, as many objects as you want, each with its own state, none of them reaching back to mutate the class.
Hold onto whichever of those sticks. Every confusing Docker error you’ll hit this month is really just a violation of that one picture.
A Dockerfile is instructions, not a thing that runs
A Dockerfile is a plain text file. It does nothing on its own. It just lists, step by step, how to assemble an environment: which base to start from, what to copy in, what to install, what command to run later.
# Dockerfile for a small Express API
# Start from the official Node 20 image, the slim Alpine Linux variant
FROM node:20-alpine
# Everything after this happens inside /app in the image
WORKDIR /app
# Copy only the dependency manifests first, so this layer caches well
COPY package*.json ./
# Install exactly what's pinned in package-lock.json
RUN npm ci
# Now copy the rest of your source
COPY . .
# Document the port your Express app listens on
EXPOSE 3000
# The command Docker runs when a container starts from this image
CMD ["node", "server.js"]
Read that top to bottom and notice what it is: a list of instructions. FROM, COPY, RUN, CMD. Nobody is running your app yet. You wrote down the recipe. The kitchen is still cold.
A couple of these instructions trip people up because they sound like they execute now. They don’t. EXPOSE 3000 does not open a port; it’s a note in the recipe saying “this app intends to use 3000.” CMD does not run anything during the build; it records the default command for later, when a container starts. Both are instructions about the future, written into the image, not actions happening as you build.
An image is the built result, frozen
When you run docker build, Docker reads the Dockerfile from top to bottom and produces an image: a read-only snapshot of a filesystem (your OS bits, your Node runtime, your code, your installed packages) plus some metadata like that default CMD.
# Build an image from the Dockerfile in the current directory (the ".")
# Tag it "myapi" with the version label "1.0"
docker build -t myapi:1.0 .
That -t flag (for “tag”) gives the image a name you can refer to later, myapi:1.0. The . at the end is the build context: the folder Docker hands to the build so COPY has something to copy from. Forget the dot and you’ll get an error that reads like Docker is mad at you for nothing; it just doesn’t know where your files are.
After that finishes, you have an image sitting in local storage. You can see it:
# List the images you've built or pulled
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
myapi 1.0 a1b2c3d4e5f6 12 seconds ago 180MB
Two things about an image that matter. First, it is read-only and frozen. Once built, it does not change. Building again doesn’t edit the old image; it produces a new one (often reusing cached layers, which is why the second build is faster). Second, an image is not running. It’s a snapshot sitting on disk, the cooked dish in the freezer, doing absolutely nothing until you ask for an instance of it.
That word “tag” is also where a classic mix-up lives. myapi:1.0 is a name for an image. It is not a container, and it is not “the running thing.” Tagging an image no more starts it than naming a recipe cooks dinner.
A container is a running instance
docker run is where motion finally happens. It takes an image and creates a container: a live process, running the image’s CMD, with its own thin writable layer stacked on top of the read-only image underneath.
# Create and start a container from the myapi:1.0 image
# Map your machine's port 3000 to the container's port 3000
docker run -p 3000:3000 myapi:1.0
Now your Express app is actually serving requests at http://localhost:3000. The -p 3000:3000 maps a port on your machine to a port inside the container, because containers are isolated by default and won’t expose anything unless you tell them to. (Ports get their own treatment in the buzzwords guide; the short version is host:container.)
The part people miss is that writable layer. The image underneath stays frozen and read-only. Anything the container writes (a log file, an uploaded image, a row of edits, a file you vim into existence while debugging) lands in that container’s private writable layer, sitting on top of the shared image. It belongs to that one container. It is not in the image, and it is not shared with any other container.
That single fact explains the bug from the top of this page. Edit a file inside a running container and you’ve written to the container’s writable layer, not the image. Throw the container away and you threw the edit away with it.
You can run as many containers from one image as you want, and each gets its own isolated writable layer:
# Three independent containers, all from the same frozen image
docker run -d -p 3001:3000 myapi:1.0
docker run -d -p 3002:3000 myapi:1.0
docker run -d -p 3003:3000 myapi:1.0
That -d runs each one detached (in the background) so it doesn’t hog your terminal. Three live processes, three separate writable layers, one image they all share underneath and none of them can alter. Class to objects, exactly.
The two verbs that connect everything
Here are the only two commands you need to keep the whole picture straight. Everything else is detail.
# RECIPE -> SNAPSHOT
# Read the Dockerfile, produce a frozen image named myapi:1.0
docker build -t myapi:1.0 .
# SNAPSHOT -> RUNNING INSTANCE
# Take that image, start a live container from it
docker run -p 3000:3000 myapi:1.0
docker build turns a Dockerfile into an image. docker run turns an image into a container. If you ever lose the thread, ask which arrow you’re on. Build goes recipe to snapshot. Run goes snapshot to instance. They are not interchangeable, and noticing which one a README is asking you to do is half of reading Docker instructions correctly.
Stopping a container is not deleting it
This is the second big surprise, and it’s the source of the “why do I have forty containers” moment everyone eventually has.
When you stop a container, it does not vanish. It stops being a running process, but the container, writable layer and all, sticks around in a stopped state.
# Gracefully stop a running container by name or ID
docker stop hungry_einstein
Run the normal listing and it looks gone:
# Lists only RUNNING containers
docker ps
But add -a (for “all”) and there it is, parked:
# Lists ALL containers, running and stopped
docker ps -a
CONTAINER ID IMAGE STATUS NAMES
9f8e7d6c5b4a myapi:1.0 Exited (0) 2 minutes ago hungry_einstein
3a2b1c0d9e8f myapi:1.0 Exited (137) 1 hour ago sad_napier
That STATUS column is genuinely useful when something crashed. Exited (0) means a clean exit. Any other number is the exit code your process died with, and 137 specifically usually means the container was killed for running out of memory. Stopped containers are a little black box of how the process ended, which is exactly why Docker keeps them around instead of deleting them out from under you.
To actually delete a stopped container, you rm it:
# Delete one stopped container (and its writable layer) for good
docker rm hungry_einstein
# Delete every stopped container at once when they pile up
docker container prune
docker rm removes the container and its writable layer. The image is untouched, sitting right where it was, ready to make more containers.
So the full lifecycle, with the right verb for each step:
docker run myapi:1.0creates and starts a new container.docker stop <name>halts it but keeps it around (visible indocker ps -a).docker start <name>resumes that same stopped container, writable layer and all.docker rm <name>deletes it permanently.docker run myapi:1.0again gives you a clean new container that knows nothing about the old one.
The comparison table to keep on your desk
| Dockerfile | Image | Container | |
|---|---|---|---|
| What it is | A text file of build instructions | A frozen, read-only filesystem snapshot plus metadata | A running instance with its own writable layer |
| How you make it | You write it by hand | docker build -t name:tag . | docker run name:tag |
| The analogy | The recipe | The cooked dish in the freezer (the class) | The plate you eat from (the object) |
| Does it run? | No, it’s just text | No, it’s a snapshot on disk | Yes, it’s a live process |
| Can it change? | You edit the file | No, rebuild for a new one | Yes, but only its own writable layer |
Classic traps
These are the ones that get nearly everyone at least once.
| What you did | What you expected | What actually happened |
|---|---|---|
| Edited a file inside a running container | The image now has your change | The change lives in that container’s writable layer; rebuild or docker rm and it’s gone |
Ran docker run again after stopping | Your previous container, resumed | A brand new container with an empty writable layer; you wanted docker start |
Called the tag myapi:1.0 “the container” | The thing that’s running | That’s the image name; the container is the instance you got from docker run |
Ran docker build to restart your app | The app comes back up | You only rebuilt the image; docker run is what starts it |
Saw nothing in docker ps and assumed it was deleted | Stopped means gone | Stopped containers hide in docker ps -a until you rm them |
Why this matters when you actually ship something
This isn’t trivia. The moment you push a Dockerized app to a host like
The platform clones your repo, runs docker build to bake your Dockerfile into an image, and then runs docker run (often several times, behind a load balancer) to start containers from that one image. When you redeploy, it builds a new image and starts new containers; it does not log into the old ones and patch them. That’s why production servers are described as disposable and “stateless,” and why anything precious (your Postgres data, your Redis cache, uploaded files) has to live in a managed database or a volume, never inside a container’s writable layer.
Get the three nouns straight and a pile of production advice suddenly makes sense on its own. Build the image once, run it anywhere, keep state outside the container, and rebuild instead of patching. It all falls out of one picture: the recipe, the frozen dish, and the plate you’re allowed to make a mess on.