Images, Containers and the Docker Hub
Chapter 2 — Images, Containers and the Docker Hub
In Chapter 1 you ran your first container and saw Docker pull an image automatically. In this chapter we go deeper: how images are actually structured, how to find and choose the right image on Docker Hub, how tags work, and how to manage the images and containers accumulating on your machine. By the end you'll have a real web server running inside a container and visible in your browser.
1. How Images Are Built — Layers
A Docker image is not a single flat file. It is a stack of read-only layers, each one representing a set of changes made on top of the previous layer. When you run a container, Docker adds one more thin writable layer on top for your changes.
This layered design has two important practical benefits:
- Sharing saves disk space. If you pull an
nginximage and aphpimage that both build on the same Debian base, Docker only stores the Debian layers once. The shared layers are reused. - Caching speeds up builds. When you update your app's config (Layer 4 above), Docker only rebuilds that layer and above — it reuses the cached nginx and OS layers. This is why the second build is always faster than the first.
2. Docker Hub — Finding the Right Image
Docker Hub (hub.docker.com) is the default public registry.
It hosts tens of thousands of images. When you type docker pull nginx,
Docker looks there first. You can also search from the command line:
The most important column is OFFICIAL. Official images are maintained by the software's own team (or Docker itself) and are the safest choice for production use. Here's how to read the results:
3. Understanding Tags
Every image on Docker Hub has one or more tags — labels
that identify different versions or variants of the same image. The format is
image:tag. If you omit the tag, Docker assumes :latest.
| Tag | What you get | Use when |
|---|---|---|
| nginx:latest | Most recent stable release (same as nginx) |
Experimenting / learning |
| nginx:1.27 | Specific version pinned | Production — you control when you upgrade ✓ recommended |
| nginx:alpine | Built on Alpine Linux — much smaller image (~45 MB vs ~190 MB) | When image size matters, e.g. Raspberry Pi |
| nginx:1.27-alpine | Specific version + Alpine base | Best of both — pinned version, small size |
| python:3.12-slim | Slim variant — fewer pre-installed packages | When you know exactly what you need |
| mysql:8.0 | MySQL 8.0.x (patch version floats) | Gets security patches automatically within 8.0 |
:latest in anything long-running.
If you use nginx:latest today and pull it again in three months,
you might get a different version. For a home web server this is fine; for
anything that needs to stay consistent, always pin a version tag.
To see all available tags for an image, visit its Docker Hub page
(e.g. hub.docker.com/_/nginx) and click the Tags tab —
official images typically have dozens.
4. Pulling and Inspecting Images
You can pull an image without running it using docker pull.
This is useful when you want to download an image in advance or fetch a
specific tag:
| REPOSITORY | TAG | IMAGE ID | CREATED | SIZE |
|---|---|---|---|---|
| nginx | alpine | a6a8fe5e09c3 | 2 weeks ago | 47.1MB |
| ubuntu | latest | 35a88802559d | 3 weeks ago | 78.1MB |
| hello-world | latest | 74cc54e27dc4 | 5 months ago | 13.3kB |
To see the layers that make up an image, use docker image history:
Reading bottom to top, you can see exactly how the image was built: start from Alpine, install nginx, copy in a config file, expose port 80, set the start command.
5. Running Your First Web Server
Let's run nginx and actually see it in a browser. The key new concept here is port mapping — telling Docker to connect a port inside the container to a port on your machine.
Now open http://localhost:8080 in your browser — you should see
the nginx welcome page. Let's break down the flags used:
-d— detached mode. The container runs in the background so your terminal is free. Without this, the container's output streams to your terminal and you can't type anything else.-p 8080:80— port mapping. Format ishost-port:container-port. nginx listens on port 80 inside the container; we map that to port 8080 on our machine. You can use any free port on the left side.--name my-nginx— give the container a memorable name. Without this, Docker assigns a random name likefunny_einstein.
-p HOST:CONTAINER-p 8080:80 → your browser visits localhost:8080,
Docker forwards traffic to port 80 inside the container.-p 3000:3000 → same port on both sides (common for dev servers).-p 127.0.0.1:8080:80 → only accessible from localhost, not the network.
6. Managing Running Containers
With a container running in the background, here are the key commands for checking on it, peeking inside it, and stopping it:
docker ps— list running containers. Add-ato also see stopped ones.docker logs my-nginx— see the container's output (access logs, errors). Add-fto follow in real time.docker exec -it my-nginx sh— open a shell inside a running container. Different fromdocker run— this attaches to an existing container rather than starting a new one. Alpine-based images useshnotbash.docker stop my-nginx— gracefully stop the container (sends SIGTERM, waits 10 seconds, then SIGKILL).docker start my-nginx— restart a stopped container. It keeps the same name, port mappings, and settings.
7. The Container Lifecycle
A stopped container still exists — it occupies a small amount of disk space
and retains its writable layer. Only docker rm truly removes it.
Removing a container does not remove the image it came from — the image
stays on disk for next time.
docker rm -f to force-remove a running container.
8. Restart Policies
If your server reboots, Docker containers don't restart automatically by default. For anything you want to stay running — a web server, a database — add a restart policy when you create the container:
For a fallback web server (like the one we'll build in Chapter 7),
--restart unless-stopped is the right choice — it comes back
automatically after a reboot, but stays stopped if you deliberately stop it.
- Run nginx and open it in a browser. Use
docker run -d -p 8080:80 --name my-nginx nginx:alpine. Visithttp://localhost:8080and confirm you see the nginx welcome page. Check the access log withdocker logs my-nginxand find the line recording your browser request. - Explore tags. Pull
nginx:latestandnginx:alpineand compare their sizes withdocker images. The alpine version should be roughly 4× smaller. Then pullpython:3.12-slimand compare it topython:3.12— again check the size difference. - Peek inside a running container. While my-nginx is running, use
docker exec -it my-nginx shto get a shell inside it. Navigate to/usr/share/nginx/html/and usecat index.htmlto read the default welcome page. Exit the container — the web server keeps running. - Practise the lifecycle. Stop my-nginx with
docker stop my-nginx, verify it appears indocker ps -aas Exited, then restart it withdocker start my-nginxand confirm it's accessible in the browser again. Finally remove it withdocker rm -f my-nginx. - Clean up your image cache. Run
docker imagesto see everything you've pulled so far. Remove thehello-worldimage withdocker rmi hello-world(remove any stopped hello-world containers first if needed). Then rundocker system dfto see a summary of how much disk space Docker is using across images, containers, and volumes.