Module 01: Images & Containers
The Lifecycle & the Core CLI
Meet Sam, a developer shipping their first containerized app. Foundations told Sam what a container is. This module is where that knowledge becomes muscle memory: the lifecycle every container moves through, and the dozen commands you'll type every working day. Reading is not enough here, so run every command yourself.
Where we left off
Sam's goal is simple: get one web app running in a container and keep it running. From Foundations, hold these three sentences in your head, because everything in this module hangs off them:
Container = process
A normal host process, isolated by namespaces, bounded by cgroups, on a copy-on-write filesystem.
Image = blueprint
An immutable stack of read-only layers. The class. You run many containers from one image.
Container = instance
A running/stopped image with a thin writable layer + its own namespaces. The object.
Memorising the flags of docker run matters far less than two things: being able to
walk through a container's lifecycle, and knowing how to debug a container that won't start. Both are below.
Image and container, made concrete
The image is layers of read-only filesystem. Starting a container adds one thin writable layer on top (copy-on-write). Everything the running process writes lands there. Delete the container and that layer vanishes (the image layers are untouched and shared with every other container started from the same image). This is the trap Sam hit first: a config file edited inside a running container disappeared on the next run.
Two containers from the same image share the three RO layers; only the top layer differs.
Data you write inside a running container is not in the image and does not survive
docker rm. Persistence is a separate concern, covered in Module 03 (volumes).
For now: treat container filesystems as disposable.
The container lifecycle
This is the single most important diagram in the module. Every command you learn is a transition between these states.
unpause
(Exited)
| State | Means | Enters via |
|---|---|---|
| (image) | Just a template on disk. No process. | docker pull / build |
| Created | Container exists, namespaces set up, but the process hasn't started. | docker create |
| Running | PID 1 is alive and executing. | docker run / start |
| Paused | Processes frozen (SIGSTOP via cgroup freezer). RAM kept. | docker pause |
| Stopped / Exited | Process ended (cleanly or killed). Writable layer still on disk. | docker stop / process exits |
| Removed | Gone. Writable layer deleted. | docker rm |
run = create + start. And the container lives exactly as long as its PID 1 (the main process) lives.
When PID 1 exits, the container stops. That's why a container running a one-shot command exits immediately,
and why docker run ubuntu stops at once but docker run nginx keeps running.
Nginx is a long-lived foreground process. Sam's app needs to stay up, so it needs a process like that as PID 1.
docker stop sends SIGTERM, waits (default 10s grace), then SIGKILL. That is the graceful path.
docker kill sends SIGKILL immediately, which is abrupt. In production you almost always want
stop so the app can flush and close connections.
Anatomy of docker run
One command does a lot. The first time Sam typed it, the flag order felt arbitrary. It isn't. Read it by position: the parts before the image name configure Docker, and everything after the image name is passed into the container.
The flags you'll reach for constantly
| Flag | Does | When |
|---|---|---|
-d | Detached: run in background, return the prompt. | Servers, anything long-lived. |
-it | -i keep STDIN open + -t allocate a TTY. | Interactive shells: docker run -it ubuntu bash. |
--name | Give a stable, human name instead of a random one. | Always, for anything you'll reference again. |
-p H:C | Publish: map host port → container port. | To reach a server from your browser (full detail in Module 04). |
-e K=V | Set an environment variable inside the container. | Config, secrets-ish (real secrets = Module 07). |
--rm | Auto-remove the container when it exits. | Throwaway / test runs, keeps your machine clean. |
--restart | Restart policy: no / on-failure / always / unless-stopped. | Resilience for long-running services. |
# Foreground: logs stream to your terminal, Ctrl-C stops it
docker run nginx
# Detached + named + port-mapped: the real-world pattern
docker run -d --name web -p 8080:80 nginx:1.27
# → open http://localhost:8080
# Interactive shell inside a fresh ubuntu (exits when you type `exit`)
docker run -it --rm ubuntu bashThe daily-driver CLI
These map one-to-one onto the lifecycle diagram. Commit the shape of each to memory.
| Command | Transition / purpose |
|---|---|
docker ps | List running containers. -a = include stopped. -q = IDs only. |
docker create IMAGE | image → Created (no process yet). |
docker start NAME | Created/Stopped → Running. |
docker stop NAME | Running → Stopped (SIGTERM → SIGKILL). |
docker restart NAME | stop then start in one call. |
docker pause / unpause | Running ⇄ Paused (freeze without stopping). |
docker rm NAME | Stopped → Removed. -f force-removes a running one. |
docker rename OLD NEW | Rename without recreating. |
docker cp src dst | Copy files between host and container. |
docker ps -a # see everything, running or not
docker stop web # graceful shutdown
docker start web # bring it back, same writable layer
docker rm -f web # nuke it, running or not
# Clean up ALL stopped containers at once
docker container pruneEvery container has a 64-char ID; you can use any unambiguous prefix (docker stop a3f).
No --name? Docker invents one like nostalgic_turing. Name things you care about.
Looking inside: exec · logs · inspect
This trio is your debugging toolkit. When a container is misbehaving, this is what you reach for.
exec
Run a new process in a running container. Your way in to poke around live.
logs
Read what PID 1 wrote to stdout/stderr. First stop for "why did it crash?".
inspect
Full JSON: config, mounts, network, state, exit code. The ground truth.
# Get a shell inside the running 'web' container
docker exec -it web sh
# Follow logs live (like tail -f); --tail limits history
docker logs -f --tail 50 web
# Pull one field out of the JSON with a Go template
docker inspect -f '{{.State.Status}} {{.State.ExitCode}}' web
running 0docker exec starts a new process (e.g. a second shell), so exiting it leaves the container running.
docker attach connects to the existing PID 1's streams, where Ctrl-C can kill the container. Sam learned this the hard way.
For debugging, you almost always want exec.
Install a package via exec and it lands in the writable layer, gone on rm.
To bake changes into an image you edit the Dockerfile and rebuild, which is Module 02. Never "fix" images by hand inside a container.
Managing images
Containers come from images; images come from a registry (or your own builds). The image-side commands:
| Command | Purpose |
|---|---|
docker pull IMAGE:TAG | Download an image (layer by layer) from a registry. |
docker images | List local images, their tags and sizes. |
docker rmi IMAGE | Remove an image (must have no containers using it). |
docker tag SRC NEW | Add another name/tag to the same image ID. |
docker image prune -a | Reclaim disk: delete dangling / unused images. |
docker history IMAGE | Show the layers and the command that made each. |
nginx:latest today and next month can be different images. For reproducibility, pin a specific
tag (nginx:1.27) or, best, a digest (nginx@sha256:…). This is a real
production pitfall. Registries, digests, and signing are Module 06.
docker images
REPOSITORY TAG IMAGE ID SIZE
nginx 1.27 a1b2c3d4e5f6 188MB
docker history nginx:1.27 # see how the layers were built
docker image prune -a # free disk (careful: removes unused images)Tying it together: what docker run nginx really does
The exact trace from Foundations, now grounded in the lifecycle. Be able to narrate this end to end:
- Client → daemon. Your CLI sends a REST request over
/var/run/docker.sock. - Image resolution. Daemon checks for
nginxlocally; if missing it pulls the layers from the registry. - Create. Daemon → containerd prepares the container: assembles the overlay filesystem + a writable layer, sets up namespaces. → state Created.
- Start. runc applies cgroups and spawns PID 1 (the nginx master process). → state Running.
- Live. Networking attached, ports published. Container lives as long as PID 1 lives.
- Exit. PID 1 ends → Stopped.
docker rmdrops the writable layer → Removed.
"run is create plus start; create sets up namespaces and the writable layer via containerd, start applies cgroups and launches PID 1 via runc, and the container survives only as long as that PID 1." That sentence signals you understand the whole stack, not just the CLI.
Hands-on: do this now
This is the exact sequence that took Sam's app from "won't stay up" to shipped. Docker is muscle memory. Run every line, predict the output before you hit enter, then check.
# 1. Start a named web server, detached, on host port 8080
docker run -d --name web -p 8080:80 nginx:1.27
# 2. Confirm it's running, then hit it
docker ps
curl -s localhost:8080 | head -n 5
# 3. Look inside: shell in, check the nginx process is PID 1
docker exec -it web sh -c 'ps -ef | head'
# 4. Watch logs, then refresh the browser/curl and watch a line appear
docker logs -f web # Ctrl-C to stop following
# 5. Lifecycle: stop → see it Exited → start → it's back
docker stop web && docker ps -a
docker start web
# 6. Prove the writable layer is disposable
docker exec web sh -c 'echo hi > /tmp/proof'
docker rm -f web
docker run -d --name web -p 8080:80 nginx:1.27
docker exec web ls /tmp # /tmp/proof is GONE: new writable layer
# 7. Clean up
docker rm -f web- You saw
nginxas PID 1 inside the container. - You watched a request appear in
logs -fin real time. - You proved a file written inside the container did not survive
rm+ re-run. - You can recite:
run = create + start, container lives = PID 1 lives.