Images and Containers
By the end of this lesson
Distinguish the package from the running instance.
An image is a read-only package. A container is a running instance of one. That distinction was introduced alongside the virtual machine comparison; this lesson goes into what each one is actually made of, because the practical rules of working with Docker fall straight out of it.
An image has two parts. The first is a stack of read-only layers holding files: a base operating system layer, then whatever each build instruction added. The second is metadata — which command to run on start-up, which user to run it as, which environment variables to set, which port the image documents.
Starting a container adds a thin writable layer on top of that stack and runs the configured command. The image itself never changes. Every write the container performs lands in its own writable layer, which belongs to that container and nothing else.
The vocabulary that appears in every Docker command from here on:
- Layer
- One read-only set of filesystem changes produced by a single build instruction. Layers stack, and identical layers are stored once and reused across images.
- Image
- A stack of layers plus metadata, identified by a content-derived digest. Read-only, and identical wherever it runs.
- Tag
- A human-readable label pointing at an image, written as name:tag — for example postgres:16. The Building Images lesson covers how movable those labels are.
- Registry
- A server that stores images so they can be shared. Docker Hub is one; cloud providers and private registries work the same way.
- Container
- One running or stopped instance of an image, with its own writable layer, its own name and its own identifier.
# Fetch an image without starting anything
docker pull postgres:16
# Three isolated databases from that one image
docker run -d --name test-db-1 -p 5441:5432 \
-e POSTGRES_PASSWORD=local-dev-placeholder postgres:16
docker run -d --name test-db-2 -p 5442:5432 \
-e POSTGRES_PASSWORD=local-dev-placeholder postgres:16
docker run -d --name test-db-3 -p 5443:5432 \
-e POSTGRES_PASSWORD=local-dev-placeholder postgres:16
# Three containers running
docker ps
# Still one image
docker images postgres- docker pull downloads the layers and stores them locally. It starts nothing — this is the package arriving, not the application running.
- Each docker run creates a separate container from the same layers. They share the read-only layers on disk, so the three cost far less space than three copies would.
- The names and host ports have to differ, because those belong to the containers rather than the image. The port inside each container is 5432 in all three cases.
- docker images still lists one postgres:16 entry. Counting containers and counting images are different questions.
Why changes inside a container do not last
# Write a file inside the first container
docker exec test-db-1 sh -c "echo note > /tmp/reminder.txt"
docker exec test-db-1 cat /tmp/reminder.txt # note
# The second container never saw it
docker exec test-db-2 cat /tmp/reminder.txt # No such file or directory
# Stopping keeps the writable layer
docker stop test-db-1 && docker start test-db-1
docker exec test-db-1 cat /tmp/reminder.txt # note
# Removing the container discards it
docker rm -f test-db-1
docker run -d --name test-db-1 -p 5441:5432 \
-e POSTGRES_PASSWORD=local-dev-placeholder postgres:16
docker exec test-db-1 cat /tmp/reminder.txt # gone- docker exec runs an extra command inside a container that is already running. It is how you look inside one without restarting it.
- The file is invisible to the second container because each container has its own writable layer. Sharing files between containers takes a volume, covered later in the course.
- Stop and start reuse the same container, so the writable layer survives. This is worth knowing, because it makes the next step surprising if you have not separated the two ideas.
- docker rm deletes the container and its writable layer. The replacement is built from the image alone, so it knows nothing about the file. The image never changed at any point.
Two things with similar names and almost nothing in common:
| Image | Container | |
|---|---|---|
| What it is | Stacked read-only layers plus start-up metadata | A process, plus one writable layer of its own |
| Can it change? | No. A new build produces a new image | Yes, and the changes belong to that container only |
| How many from one | One image, any number of containers | Each container comes from exactly one image |
| Lifetime | Until you delete it, or the registry does | Until it is removed — which in production is routine |
| Listed by | docker images | docker ps, or docker ps -a to include stopped ones |
| Where the definition lives | A Dockerfile in version control | The run command or Compose file that started it |
Summary
- An image is read-only layers plus start-up metadata; a container is a process with one writable layer
- Any number of containers can run from one image, sharing its layers on disk
- A container's writable layer survives stop and start, and dies with the container
- Settings that are true everywhere belong in the image; settings that vary belong in the environment
- Editing a running container by hand leaves no record and does not survive replacement
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Try it yourself
Two containers, one image
Start two containers from the same image with different names. Create a file inside the first one using docker exec.
Check whether the second container can see it. Then remove the first container, start a replacement with the same name, and check again.
Show solution
The second container cannot see the file, and the replacement cannot either. Each container gets its own writable layer, and removing a container throws that layer away.
The point of doing this by hand is that it makes the phrase "containers are disposable" concrete. It is not a recommendation about how to treat them — it is a description of what happens to anything you put inside one.
docker run -d --name api-a alpine:3.20 sleep 600
docker run -d --name api-b alpine:3.20 sleep 600
docker exec api-a sh -c "echo hello > /tmp/scratch.txt"
docker exec api-a cat /tmp/scratch.txt
docker exec api-b cat /tmp/scratch.txt
docker rm -f api-a
docker run -d --name api-a alpine:3.20 sleep 600
docker exec api-a cat /tmp/scratch.txtThink about it
Think about it
A colleague fixes a failing employees API in a test environment by opening a shell in the container and correcting a connection string in a configuration file. The API recovers. Two days later it fails the same way.
What happened, and where should the fix have gone?
Show solution
Something replaced the container — a redeployment, a restart of the host, a scaling event. The new container came from the image, which still carried the wrong connection string.
A connection string differs between environments, so it does not belong in the image either. It belongs in configuration supplied at run time: an environment variable or a mounted file, set per environment.
There is a second cost worth naming. While the hand-edited container was running, the deployed image no longer matched what was actually in production. Anyone reading the repository would have drawn the wrong conclusion about the state of the system.
Knowledge check
Nothing is recorded and there is no score. The explanation appears either way.
Saved in this browser only.