Phase 2 · CONTAINERS & CI/CD
Docker fundamentals — images, containers, registries
By the end of today
- Pull an image and run, list, stop and remove containers from it
- Explain the image-versus-container split like a class and its instances
- Read container logs and get a shell inside with exec -it
Images and containers: a blueprint and the things built from it
Yesterday you saw why containers exist — one isolated process sharing the host kernel instead of booting a whole VM. Today you drive Docker for real, and it starts with the one distinction everything else hangs off: an image is not a container.
An image is a read-only template — your application plus everything it needs to run, frozen into a stack of layers: a base OS slice, the runtime, your code, and the command that starts it. It sits on disk doing nothing, like a class definition in code or a recipe card in a binder. You build images or pull them, but you never run an image directly.
A container is a running (or stopped) instance of an image. docker run takes the read-only image, adds a thin writable layer on top, and starts the process inside it. One image spawns as many containers as you like — the way one class makes many objects — and each gets its own writable layer, its own name, its own life. Delete a container and the image is untouched, ready to start another.
That gives you a lifecycle worth memorising: pull an image, run it to make a container, ps to list what’s running, stop to halt it, rm to delete the container, and images / rmi to manage the images themselves. Two flags shape how you run: -d detaches so the container runs in the background instead of holding your terminal, and --name gives it a memorable handle instead of a random one like dreamy_hopper.
Real world: An image is the master recipe card in a bakery’s binder; a container is one cake actually baking in an oven. The card never changes and never gets eaten — you photocopy it and bake ten cakes from the one recipe at once, each in its own tin. Throwing out a burnt cake doesn’t touch the recipe; the next batch starts clean from the same card.
Where do images come from? A registry — a server that stores and serves images. The default public one is Docker Hub, and docker pull nginx reaches out to it, downloads the image, and caches it locally so the next run is instant. Companies run their own private registries too, but the commands are identical; only the address in front of the name changes.
Each image is addressed by repository and tag: nginx:1.27 names the repository nginx at tag 1.27. Leave the tag off and Docker assumes :latest — which is not “the newest” so much as “whatever the publisher last pushed as latest,” a moving target. In production you pin an explicit tag (nginx:1.27-alpine) so the image you tested is the image that ships; :latest is fine for a throwaway experiment and a trap in a deploy.
A named example makes it concrete. The official nginx image on Docker Hub is maintained by the nginx team, rebuilt whenever a base layer gets a security fix, and published under dozens of tags at once — 1.27, 1.27-alpine, latest — all pointing at real builds. That is why docker pull nginx:1.27-alpine gives you the same audited web server on your laptop, in CI, and on a production node: the registry, not the machine, decides what those bytes are.
Two commands, one mental model: you fetch immutable images from a registry, then run mutable containers from them locally. Get that split straight and every Docker command from here — build, network, volume — is just a variation on it.
Hands-On Lab
Budget about 25 minutes. Open your WSL2 Ubuntu 24.04 terminal with Docker 27+ installed (Day 22’s setup). Type each command yourself and read every line of output; today the goal is to feel the image → container lifecycle, not to memorise flags. Identifiers like image IDs, digests, container IDs and timestamps are unique to each build and run — yours will differ from the samples below.
# 1. Confirm Docker is installed and the daemon is answering (client AND server must appear).
docker version
# Output (trimmed; your versions will differ):
# Client: Docker Engine - Community
# Version: 27.5.1
# API version: 1.47
# Server: Docker Engine - Community
# Engine:
# Version: 27.5.1
# 2. Pull the official nginx image from Docker Hub, pinned to an explicit tag.
docker pull nginx:1.27-alpine
# Output (layer IDs and the digest are content-addressed — yours will differ):
# 1.27-alpine: Pulling from library/nginx
# 6e771e15690e: Pull complete
# a1c...: Pull complete
# Digest: sha256:… (unique to this build — never memorise or hard-code it)
# Status: Downloaded newer image for nginx:1.27-alpine
# docker.io/library/nginx:1.27-alpine
# 3. List the images now on disk — you should see the one you just pulled.
docker images
# Output (IMAGE ID and SIZE vary by build and CPU architecture):
# REPOSITORY TAG IMAGE ID CREATED SIZE
# nginx 1.27-alpine <image-id> 2 weeks ago ~52MB
# 4. Run a container from that image: -d detaches (background), --name gives it a handle.
docker run -d --name web nginx:1.27-alpine
# Output (a 64-char container ID printed once — yours will differ):
# 3f9a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8
# 5. List RUNNING containers. Note the image it came from, its status and its name.
docker ps
# Output (short IDs and timestamps will differ):
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 3f9a1b2c3d4e nginx:1.27-alpine "/docker-entrypoint.…" 5 seconds ago Up 4 seconds 80/tcp web
# 6. Read what the container's main process has logged to stdout/stderr.
docker logs web
# Output (representative nginx startup lines):
# /docker-entrypoint.sh: Configuration complete; ready for start up
# 2026/07/11 09:14:22 [notice] 1#1: using the "epoll" event method
# 2026/07/11 09:14:22 [notice] 1#1: nginx/1.27.4
# 2026/07/11 09:14:22 [notice] 1#1: start worker processes
# 7. Get a shell INSIDE the running container. -i keeps stdin open, -t allocates a terminal.
docker exec -it web sh
# Output (your prompt changes to the container's shell — you are now inside it):
# / # grep '^PRETTY_NAME=' /etc/os-release
# PRETTY_NAME="Alpine Linux v3.20" (the version tracks the nginx tag — yours may differ)
# / # exit # leaves the shell; the container keeps running
# 8. Stop the container. Docker echoes the name it stopped.
docker stop web
# Output:
# web
# 9. Plain `docker ps` now hides it — add -a to see stopped containers too.
docker ps -a
# Output (STATUS shows it exited cleanly with code 0):
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 3f9a1b2c3d4e nginx:1.27-alpine "/docker-entrypoint.…" 2 minutes ago Exited (0) 8 seconds ago web
# 10. Remove the stopped container. This deletes the container, NOT the image.
docker rm web
# Output:
# web
# 11. Prove the split: the container is gone, but its image is still on disk, ready to reuse.
docker images
# Output (the image survived — `docker rmi nginx:1.27-alpine` would remove it too):
# REPOSITORY TAG IMAGE ID CREATED SIZE
# nginx 1.27-alpine <image-id> 2 weeks ago ~52MB
Read the last two outputs back to yourself: you pulled one immutable image, ran a disposable container from it, went inside it, stopped it, and deleted it — yet the image is still sitting there, ready to spawn the next container. That is the whole image → container lifecycle you’ll repeat for the rest of this phase.
Common Errors & Fixes
These three trip up almost everyone in their first week with Docker. Read the error text slowly — learning to parse it is the actual skill.
Common error: Running any docker command before the daemon is up (or as a user not in the
dockergroup):Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?Why: The
dockercommand is a thin client that talks to a background daemon (dockerd) over a socket. If the daemon isn’t started, or your user can’t read the socket because it isn’t in thedockergroup, the client has nothing to connect to and says so — this is not about the image or the command you typed.Fix: Start the daemon (
sudo service docker starton WSL2, or launch Docker Desktop;sudo systemctl start dockeron a Linux VM). To drop thesudo, add yourself to the group once withsudo usermod -aG docker $USER, then open a new session so the membership takes effect.How you’d spot it in prod: A CI job or deploy that suddenly fails every docker command with this exact message means the daemon died or the runner lost socket access — check
systemctl status dockerand the runner’s group membership before touching the pipeline itself.
Common error: Re-running
docker run --name web …when a container already owns that name — even a stopped one:docker: Error response from daemon: Conflict. The container name "/web" is already in use by container "3f9a1b2c3d4e…". You have to remove (or rename) that container to be able to reuse that name.Why:
--namemust be unique across all containers, running or stopped. A stopped container still reserves its name until you remove it, so the secondruncollides with the first.Fix: Pick a new name, or clear the old one first —
docker rm web(add-fto force-remove one that’s still running), then re-run. For throwaway containers,docker run --rmauto-deletes on exit so the name is freed immediately.How you’d spot it in prod: A deploy script that runs
docker run --name appon every release works the first time and fails on the second, because it never removed the previous container. The fix is adocker rm -f appbefore the run, or moving to Compose which reconciles by name for you.
Common error: Pulling an image with a typo in the repository name — here
ngnixinstead ofnginx:Error response from daemon: pull access denied for ngnix, repository does not exist or may require 'docker login': denied: requested access to the resource is deniedWhy: Docker looked up the name literally on Docker Hub, found no public repository called
ngnix, and — because a missing public repo is indistinguishable from a private one you aren’t allowed to see — it reports access denied rather than “not found.” The wording sends people hunting for credentials when the real problem is a spelling slip.Fix: Check the exact name against Docker Hub and retype it:
docker pull nginx. For genuinely private images, authenticate first withdocker login, then pull.How you’d spot it in prod: A build failing with
pull access deniedon an image that “definitely exists” is usually a typo, a missing registry prefix, or a private image the runner isn’t logged in for. Verify the exactrepository:tagbefore you start adding registry credentials.
Docker Fundamentals Interview Questions
Cover the answers below and say your own version out loud first — define an image versus a container, and what :latest really means, before you reveal each answer. Recalling before revealing is what makes these stick when an interviewer asks them cold. The four questions and answers render right after this note.
Go Deeper
Optional extras if you have ~30 more minutes today:
- 5 min — Run
docker run --rm hello-worldand read its message end to end; it narrates exactly what the client, daemon and registry each did to get those words on your screen. - 10 min — Run
docker history nginx:1.27-alpineto see the image’s layer stack, thendocker inspect webon a running container to see the config Docker built around it — the image and the instance, side by side. - 15 min — Read the “Images, Layers” opening of the Docker for DevOps guide for how images, tags and the layer cache fit the wider container picture you’ll build on tomorrow.
What is the difference between a Docker image and a container? Both
An image is a read-only template — your app plus its dependencies and a start command, frozen into layers. A container is a running instance of that image with a thin writable layer on top. The analogy I use is a class and its objects: one image, many containers. docker pull or docker build gives you images; docker run turns an image into a container. Deleting a container leaves the image untouched, so I can start a fresh one instantly. That split matters operationally — images are immutable and shippable, containers are disposable and often stateful, which is why you never keep important data in a container's writable layer without a volume.
What does the :latest tag mean, and why not rely on it in production? Both
latest is just the default tag Docker uses when you don't specify one — docker pull nginx really means nginx:latest. The trap is that latest isn't 'the newest version'; it's whatever the publisher last tagged as latest, so it moves under you. Two machines pulling nginx:latest a month apart can get different images, which quietly breaks reproducibility. In production I pin an explicit, immutable tag like nginx:1.27-alpine, or better a digest, so the image I tested is exactly the image that ships. latest is fine for a quick local experiment, but in a Dockerfile FROM or a deploy it's a classic source of 'works on my machine' bugs.
How do you get inside a running container to debug it? Service
I use docker exec -it <container> sh, or bash if the image has it. exec runs a new process inside an already-running container; -i keeps stdin open and -t allocates a terminal, so together they give me an interactive shell. From there I can read config, look at files, or run the app's own CLI. The key distinction is exec versus run: run starts a brand-new container, exec attaches to one that's already up — reaching for run when I meant exec is a common slip. For a container that has already crashed I can't exec into it, so I read docker logs <container> instead, which shows what the main process wrote to stdout and stderr.
What is a container registry, and what is Docker Hub? Both
A registry is a server that stores and distributes images — you push images to it and pull them from it. Docker Hub is the default public registry, so docker pull nginx fetches from it automatically. Images are addressed as repository:tag, like nginx:1.27, and a registry can host public images anyone can pull or private ones that need docker login first. In real work you'll also meet private registries — cloud providers and code hosts each run one — but the commands don't change; only the address in front of the image name does. The mental model is simple: the registry is the shared library, docker pull borrows a copy, and docker run brings it to life locally.
Quick check
Mark Day 23 complete
Tomorrow you stop pulling other people's images and write your own — a Dockerfile bakes your app into an image that runs anywhere.
Stuck on today’s lab? Ask in Mission 90 Q&A