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.

Container writable layer (your changes) ephemeral — gone when container is removed
Layer 4 — App config (nginx.conf, site files) ~2 MB
Layer 3 — nginx installed ~45 MB
Layer 2 — Debian base libraries ~80 MB
Layer 1 — Scratch / base OS filesystem ~30 MB

This layered design has two important practical benefits:

  • Sharing saves disk space. If you pull an nginx image and a php image 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.
Copy-on-write: When a running container modifies a file that exists in a read-only layer, Docker copies that file up into the writable layer and modifies it there. The original image layer is never touched. This is called copy-on-write (CoW) and it's why you can run ten containers from the same image without ten copies of the image.

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:

Terminal
$ docker search nginx NAME DESCRIPTION STARS OFFICIAL nginx Official build of Nginx... 20138 [OK] unit Official build of NGINX Unit... 76 [OK] nginx/nginx-ingress NGINX and NGINX Plus Ingress... 96 bitnami/nginx Bitnami container image for... 198 ...

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:

🐳
nginx
Official build of Nginx. High performance web server and reverse proxy.
✓ Official Image ⬇ 1B+ pulls ★ 20,138
📦
bitnami/nginx
Bitnami container image for Nginx. Includes non-root user, health checks, extra tooling.
Community Image ⬇ 100M+ pulls ★ 198
Stick to official images when starting out. They're well documented, regularly updated with security patches, and use minimal base images to keep sizes small. For the scenarios in this course (Apache, Python, MySQL) we'll always use the official images.

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
Avoid :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:

Terminal
$ docker pull nginx:alpine alpine: Pulling from library/nginx 2d429b9e73a6: Pull complete # Layer 1 — Alpine base 20c8b3871098: Pull complete # Layer 2 — nginx binaries 06da587a7970: Pull complete # Layer 3 — nginx config Digest: sha256:a45ee5d042aaa9e81... Status: Downloaded newer image for nginx:alpine $ docker images
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:

Terminal
$ docker image history nginx:alpine IMAGE CREATED CREATED BY SIZE a6a8fe5e09c3 2 weeks ago CMD ["nginx" "-g" "daemon off;"] 0B <missing> 2 weeks ago EXPOSE map[80/tcp:{}] 0B <missing> 2 weeks ago COPY nginx.conf /etc/nginx/... 1.01kB <missing> 2 weeks ago RUN apk add --no-cache nginx 15.2MB <missing> 2 weeks ago FROM alpine:3.19 7.38MB

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.

Terminal
$ docker run -d -p 8080:80 --name my-nginx nginx:alpine a91b3c2d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b

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 is host-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 like funny_einstein.
Port mapping syntax: -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:

Terminal
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES a91b3c2d4e5f nginx:alpine "/docker-entrypoint.…" 2 minutes ago Up 2 minutes 0.0.0.0:8080->80/tcp my-nginx $ docker logs my-nginx 2024/06/16 10:32:14 [notice] 1#1: start worker process 6 172.17.0.1 - - [16/Jun/2024:10:32:22] "GET / HTTP/1.1" 200 615 "-" "Mozilla/5.0..." $ docker exec -it my-nginx sh / # ls /etc/nginx/ conf.d fastcgi.conf mime.types nginx.conf ... / # exit $ docker stop my-nginx my-nginx $ docker start my-nginx my-nginx
  • docker ps — list running containers. Add -a to also see stopped ones.
  • docker logs my-nginx — see the container's output (access logs, errors). Add -f to follow in real time.
  • docker exec -it my-nginx sh — open a shell inside a running container. Different from docker run — this attaches to an existing container rather than starting a new one. Alpine-based images use sh not bash.
  • 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

docker pull
Image on disk
ready to use
docker run
Running
process active
docker stop
Stopped
still exists
docker start
Running again
same settings
docker rm
Gone
image still there

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.

# Remove a stopped container docker rm my-nginx # Stop and remove in one command docker rm -f my-nginx # Remove all stopped containers at once docker container prune # Remove an image docker rmi nginx:alpine # Remove all unused images (not referenced by any container) docker image prune -a
You can't remove an image while a container using it exists — even if the container is stopped. Remove the container first, then the image. Or use 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:

# always restart (even after Docker Desktop restarts) docker run -d -p 8080:80 --name my-nginx --restart always nginx:alpine # restart unless you manually stopped it docker run -d -p 8080:80 --name my-nginx --restart unless-stopped nginx:alpine # only restart on failure (not if you stopped it manually) docker run -d -p 8080:80 --name my-nginx --restart on-failure nginx:alpine

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.

Exercises
  1. Run nginx and open it in a browser. Use docker run -d -p 8080:80 --name my-nginx nginx:alpine. Visit http://localhost:8080 and confirm you see the nginx welcome page. Check the access log with docker logs my-nginx and find the line recording your browser request.
  2. Explore tags. Pull nginx:latest and nginx:alpine and compare their sizes with docker images. The alpine version should be roughly 4× smaller. Then pull python:3.12-slim and compare it to python:3.12 — again check the size difference.
  3. Peek inside a running container. While my-nginx is running, use docker exec -it my-nginx sh to get a shell inside it. Navigate to /usr/share/nginx/html/ and use cat index.html to read the default welcome page. Exit the container — the web server keeps running.
  4. Practise the lifecycle. Stop my-nginx with docker stop my-nginx, verify it appears in docker ps -a as Exited, then restart it with docker start my-nginx and confirm it's accessible in the browser again. Finally remove it with docker rm -f my-nginx.
  5. Clean up your image cache. Run docker images to see everything you've pulled so far. Remove the hello-world image with docker rmi hello-world (remove any stopped hello-world containers first if needed). Then run docker system df to see a summary of how much disk space Docker is using across images, containers, and volumes.
Next: Chapter 3 — Essential Docker Commands
You now know how to pull images, run containers, check their status, and clean up. Chapter 3 is your practical command reference — covering run, ps, stop, rm, exec, logs, inspect, and cp with real examples for each, plus the most useful flags you'll reach for every day.