Docker — Package an app and its dependencies into an image
| Levels | Beginner → Intermediate → Production |
| Time | Beginner ~25 min · full module ~3h |
| Prerequisites | Linux first success; Docker Engine or Desktop |
| You will be able to | (1) explain image vs container vs layer (2) write a small Dockerfile and run it (3) say when Compose is better than a long docker run |
Last verified: 2026-08-16 · Tested with: Docker Engine 27.x / 28.x
60-second overview
Docker packages an app and the libraries it needs into an image, then runs a copy of that image as a container. The host kernel is shared; the filesystem and process list are not. You use it so “works on my laptop” and “works on the server” are the same filesystem, not two slightly different apt installs.
Mental model
An image is a sealed lunchbox; a container is someone eating from a copy of that lunchbox. Layers are ingredients stacked so two lunchboxes can share the bread.
flowchart LR Dockerfile --> Image --> Container Image --> Registry
Skip to
| Band | What you get | Go |
|---|---|---|
| Beginner | Concepts + first success | below |
| Intermediate | Worked examples, comparisons | below |
| Production | Security, scale, real constraints | below |
Beginner: core concepts
Image vs container
- What it is: An image is an immutable filesystem snapshot plus a default command. A container is a running (or stopped) instance of that image.
- Why it exists: You build the image once, run it many times, and throw the container away. State you care about does not live in the container.
- How it looks:
docker run --rm -d --name web -p 8080:80 nginx:1.27-alpinestarts a container from thenginx:1.27-alpineimage.--rmdeletes the container on stop. - Common confusion: Deleting a container does not delete the image.
docker psis running containers;docker imagesis images.
Dockerfile
- What it is: A text file of instructions that produce an image. Typical first three:
FROM(base image),COPY(your files),CMD(what to run). - Why it exists: So the image is reproducible. “I apt-installed it by hand inside a container” is not a build.
- How it looks: examples/hello-static/Dockerfile — two lines: start from nginx, copy index.html.
- Common confusion: The Dockerfile does not run your app.
docker buildproduces an image;docker runstarts a container from it.
Layers and cache
- What it is: Each Dockerfile instruction usually becomes one layer, a filesystem diff. Docker reuses a layer when the instruction and every layer above it are unchanged.
- Why it exists: Rebuilds stay fast, and ten images that start
FROM nginx:1.27-alpineshare that base on disk. - How it looks: Put the lines that change least first (
FROM,COPY package.json,RUN npm ci) and your source last.docker buildprintsCACHEDon reused steps. - Common confusion:
COPY . .early in the file busts the cache on every edit, including README typos. That is what.dockerignoreis for.
Ports and localhost publishing (-p)
- What it is: A process in a container listens on a container port.
-p 8080:80publishes host port 8080 to container port 80. - Why it exists: Containers have their own network namespace. Without
-p(or Composeports:), only other containers on the same network can connect. - How it looks:
curl http://127.0.0.1:8080hits the host port. Inside the container the process still sees:80. - Common confusion: Publishing is not a firewall hole on another machine by itself, but
0.0.0.0:8080is reachable from other hosts on your LAN.localhostinside the container is the container, not your laptop.
Volumes vs bind mounts (light)
- What it is: A volume is Docker-managed storage. A bind mount is a path on the host, mounted into the container.
- Why it exists: Container filesystems are thrown away. Databases and your source tree need to outlive
docker rm. - How it looks: examples/docker-compose.yml bind-mounts
./hello-staticonto nginx’s html directory. Editindex.htmlon the host; nginx serves the new file. - Common confusion: A bind mount hides whatever the image had at that path. Named volumes are better for database data; bind mounts are better for “I am editing this directory.”
Beginner: first success
Goal: Run an official image on localhost, then build and run the static example.
Time: ~15 minutes.
If docker version fails with permission denied while talking to /var/run/docker.sock, your user is not in the docker group. On Ubuntu: sudo usermod -aG docker "$USER", then log out and back in. If Docker is not installed, use Docker Engine (or Desktop). The Engine convenience script is the laptop path: curl -fsSL https://get.docker.com | sh.
docker version
docker run --rm -d --name web -p 8080:80 nginx:1.27-alpine
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080
docker logs web --tail 5
docker stop web
Then, from this module directory (03-docker/):
cd examples/hello-static
docker build -t hello-static:1 .
docker run --rm -d --name hello -p 8081:80 hello-static:1
curl -s http://127.0.0.1:8081
docker stop hello
Expected output: docker version prints a Client and a Server section. The first curl prints 200. docker logs web shows a GET to /. The second curl prints the hello-static heading from index.html.
If it failed:
port is already allocated→ something else owns 8080 or 8081. Use-p 8088:80(and curl that host port).Cannot connect to the Docker daemon→ Engine is not running, or you are not in thedockergroup (see above).curl: (7) Failed to connectright afterdocker run→ wait one second and retry; nginx was still starting.
Intermediate: go deeper
Compose instead of a long docker run
examples/docker-compose.yml starts nginx and bind-mounts hello-static into the html directory. From 03-docker/examples/:
docker compose up -d
curl -s http://127.0.0.1:8082
docker compose logs --tail 5
docker compose down
Compose is the right tool when you are tired of retyping -p and -v, or when a second service (a database, a cache) needs to share a network with the app.
.dockerignore
examples/hello-static/.dockerignore keeps .git and markdown out of the build context. Without it, COPY . . sends everything in the directory to the daemon — including secrets and node_modules. Treat it like .gitignore for docker build (Git ignore example).
logs, exec, inspect
docker logs hello --tail 20
docker exec hello ls /usr/share/nginx/html
docker inspect hello --format '{{.State.Status}} {{.NetworkSettings.IPAddress}}'
logs is stdout/stderr of PID 1 in the container. exec runs an extra process in that container (the container must still be running). inspect is the full JSON; --format pulls one field.
Comparison
| Reach for a Dockerfile when… | Reach for Compose when… | Reach for Kubernetes when… |
|---|---|---|
| You are defining one image | You need one command to start an app plus its companions on a laptop (ports, bind mounts, a shared network) |
You need those containers scheduled, healed, and rolled out across machines — see Kubernetes |
docker run is the learning tool and the one-off. It is not how you describe a two-service dev stack.
Production
You should already be able to: run a container, write a 10-line Dockerfile, explain layers.
Multi-stage builds
A builder stage has compilers and node_modules. The final stage copies only the artifact. examples/multi-stage.Dockerfile compiles a tiny Go HTTP server and copies the one binary into alpine:3.20. The same idea in Node is npm run build then COPY --from=builder /app/dist.
# from 03-docker/examples/
docker build -f multi-stage.Dockerfile -t hello-multi:1 .
docker run --rm -d --name multi -p 8083:8080 hello-multi:1
curl -s http://127.0.0.1:8083
docker stop multi
The final image should not contain go, gcc, or your test fixtures.
Non-root USER
The last stage of that Dockerfile runs USER app. If the process does not need to be root, it should not be. Official nginx already drops the worker to user nginx; your own app images often forget USER and stay root.
Pin tags, never latest
nginx:1.27-alpine is a moving patch line, but it is still a pin compared to nginx:latest. latest on your laptop and latest on the server will diverge. Prefer a digest (nginx@sha256:…) when you need bit-for-bit reproducibility.
Read-only rootfs and dropped capabilities (short)
docker run --rm --read-only --tmpfs /tmp --cap-drop ALL \
--name hello-ro -p 8081:80 hello-static:1
nginx may also need tmpfs mounts for /var/cache/nginx and /run. --cap-drop ALL removes Linux capabilities the process does not need; add back only what it must have (rarely NET_BIND_SERVICE if it listens on a port below 1024 and is non-root). If the container exits, docker logs will tell you which path it tried to write.
Scan mention
Tools such as Trivy report known CVEs in an image (trivy image hello-static:1). You do not need to install a scanner for this module. Make scanning a CI step later in GitHub Actions, not a ritual you run by hand on every local build.
.dockerignore again
Production images still start with a small context. A forgotten .env in the build context is a leaked secret even if you never COPY it, because people later change the Dockerfile.
Pitfalls
| Symptom | Likely cause | Fix |
|---|---|---|
| Image is hundreds of MB for a static page | Fat base, no .dockerignore, leftover build tools |
Alpine or a multi-stage final image; add .dockerignore |
permission denied on docker.sock |
Your user is not in the docker group |
sudo usermod -aG docker "$USER" then a new login |
| Container exits immediately | PID 1 ended or crashed | docker logs NAME; docker inspect NAME --format '{{.State.ExitCode}}' |
port is already allocated |
Host port is taken | docker ps and ss -tuln; pick another -p |
How this connects
- Previous: Linux — you already know processes, ports (
ss), and files. Git — the Dockerfile belongs in a repo. - Next: Kubernetes — a Pod runs this image. GitHub Actions is where you will
docker buildand push on each git event. - When not to use this: A one-off debug on the host (
journalctl,ss, a local binary) does not need Docker. Do not wrap a singlecurlin an image to look official.
Practice
Full write-ups (setup, task, hint, success, solution notes) live under exercises/.
| # | Band | Exercise |
|---|---|---|
| 1 | Basic | Write a static Dockerfile |
| 2 | Basic | Build and publish a host port |
| 3 | Intermediate | Two services with Compose |
| 4 | Production | Four notes and a read-only run |
Cheat sheet
Command index: cheatsheet.md.
Official documentation
- Start here: What is a container? — image vs container in Docker’s own words.
- Deep reference: Dockerfile — every instruction.
- Deep reference: multi-stage builds — the Production pattern above.
- Deep reference: Compose file —
services,ports,volumes.