Session 5 — Deep Dive: Docker in Practice
Read this after the main session 5 handout. This isn’t a command catalog —
docker --helpalready exists. It’s the small set of commands and ideas you actually use day to day, with the why behind each and the gotchas that bite people. Work through it once end-to-end (~45 min); after that it’s a quick refresher.
The example we’ll use throughout
Everything here runs against public images — no accounts, nothing to build. We mostly drive one container, web, and bring in mongo when we need data and a second service:
docker run -d --name web -p 8080:80 nginx:alpine # → http://localhost:8080
Start that now and follow along.
1. The commands you’ll actually use
You’ll reach for these ten constantly. Everything else you can look up when you need it.
| Command | What it’s for | Example |
|---|---|---|
docker run | Create and start a container | docker run -d --name web -p 8080:80 nginx:alpine |
docker ps | What’s running (-a includes stopped) | docker ps -a |
docker logs | See its output (-f to follow live) | docker logs -f web |
docker exec | Get a shell / run a command inside | docker exec -it web sh |
docker inspect | Full config as JSON (pipe to jq) | docker inspect web | jq '.[0].State' |
docker stop | Graceful stop (SIGTERM, then SIGKILL) | docker stop web |
docker rm | Delete a container (-f forces) | docker rm -f web |
docker build | Build an image from a Dockerfile | docker build -t myapp:1.0 . |
docker images | List local images | docker images |
docker pull | Download an image | docker pull nginx:alpine |
The debugging muscle memory:
docker ps→ is it running?docker logs web→ what’s it saying?docker exec -it web sh→ poke around inside. Nine times out of ten that’s the whole investigation.
2. docker run — the flags worth knowing
docker run has dozens of flags. These are the ones that earn their keep:
| Flag | What it does | Example |
|---|---|---|
-d | Run in the background | docker run -d nginx:alpine |
--name | Give it a name (else you get silly_morse) | docker run -d --name web nginx:alpine |
--rm | Auto-delete when it exits (great for one-offs) | docker run --rm alpine echo hi |
-it | Interactive shell | docker run -it --rm alpine sh |
-p <host>:<container> | Publish a port | docker run -p 8080:80 nginx:alpine |
-e / --env-file | Pass config in | docker run -e LOG_LEVEL=debug alpine env |
-v | Mount a volume or host folder (Part 3) | docker run -v data:/data mongo:7 |
--restart | Auto-restart policy | docker run -d --restart unless-stopped nginx:alpine |
-m / --cpus | Cap memory / CPU | docker run -m 256m --cpus 0.5 nginx:alpine |
--health-cmd | Tell Docker how to check it’s alive | `—health-cmd ‘wget -qO- localhost/ |
Bind to localhost in dev.
-p 8080:80listens on all interfaces — anyone on your network can hit it. Use-p 127.0.0.1:8080:80so it’s reachable only from your own machine.
Going to production? Add a hardening layer the kernel enforces for you:
docker run -d --name web \
-p 127.0.0.1:8080:80 \
--read-only --tmpfs /tmp --tmpfs /var/cache/nginx \
--cap-drop ALL --cap-add NET_BIND_SERVICE \
--security-opt no-new-privileges \
--memory 256m --cpus 0.5 \
nginx:alpine
That’s: read-only filesystem, no Linux capabilities except binding a port, no privilege escalation, capped resources. If the app is ever compromised, the blast radius is tiny.
3. Volumes — don’t lose your data
A container’s filesystem is throwaway. docker rm it and everything it wrote is gone. To keep data — or share files with your host — you mount storage. Two kinds matter:
Named volume — for data you want to keep (databases, uploads). Docker manages where it lives.
docker volume create app-data
docker run -d --name mongo -v app-data:/data/db mongo:7
docker rm -f mongo # delete the container...
docker run -d --name mongo -v app-data:/data/db mongo:7 # ...data is still there
Bind mount — for sharing a host folder (source code, config). Edit on the host, the container sees it instantly.
mkdir site && echo "<h1>hello</h1>" > site/index.html
docker run -d --name web -p 8080:80 \
-v $(pwd)/site:/usr/share/nginx/html:ro \
nginx:alpine
# edit site/index.html, refresh — no restart needed. `:ro` = read-only.
The gotcha that wastes an afternoon: a bind mount hides whatever was at that path in the image. Mount your host folder over a directory the image built (e.g. installed dependencies) and the image’s version vanishes — only your host files show. On Mac, bind mounts are also noticeably slow; for heavy use, named volumes or OrbStack/Colima are much faster.
Backup tip: spin up a throwaway container to tar a volume — the standard trick:
docker run --rm -v app-data:/data:ro -v $(pwd):/backup \
alpine tar czf /backup/data.tgz -C /data .
4. Networking — let containers talk by name
One lesson matters more than all the rest:
The default network has no DNS. A network you create yourself does. On a user-defined network, containers find each other by container name — no IP juggling.
# ❌ default bridge — name lookup fails
docker run -d --name mongo mongo:7
docker run --rm alpine ping -c1 mongo # "bad address 'mongo'"
# ✅ your own network — DNS just works
docker network create app-net
docker run -d --name mongo --network app-net mongo:7
docker run --rm --network app-net alpine ping -c1 mongo # resolves!
So a real app on app-net connects with a hostname, not an IP:
docker run -d --name api --network app-net \
-e MONGODB_URI=mongodb://mongo:27017/mydb \
myapp:1.0
This is exactly what docker compose does for you — it creates one network per project and wires the names automatically. (And -p only matters for reaching a container from your host; containers on the same network talk directly, no -p needed.)
5. Building images that stay small and fast
The single most useful build idea is layer caching. Each Dockerfile line is a cached layer; Docker reuses every layer until the first one that changed, then rebuilds the rest. So order from least- to most-frequently-changing:
FROM node:21-alpine
WORKDIR /app
COPY package.json package-lock.json ./ # changes rarely
RUN npm ci # ← stays cached across code edits
COPY . . # changes constantly
RUN npm run build
If you copied everything first (COPY . .) then installed, every code edit would re-run the slow install. Copying the lockfile first means dependencies are only reinstalled when they actually change.
Two more high-leverage habits:
.dockerignore— keepnode_modules,.git, build output out of the build context. Smaller context = faster builds and smaller images.ARGis not a secret. Build args show up indocker history. For tokens/keys at build time, use a BuildKit secret mount (RUN --mount=type=secret,...) so it never lands in a layer.
6. When it breaks — the debugging playbook
Start every container problem with these three:
docker ps # running? restarting? exited?
docker logs -f --tail 200 web # what does it actually say?
docker inspect web | jq '.[0].State' # ExitCode? OOMKilled? restart loop?
OOMKilled: true → it blew past --memory. A non-zero ExitCode → the command inside failed; the logs say why.
Common patterns and their fixes:
| Symptom | Likely cause | Fix |
|---|---|---|
| Exits immediately with code 0 | Nothing keeps it alive — the command finished | Run a long-lived process; for one-offs use --rm |
| Restart loop | The startup command fails | docker logs — usually a missing env var |
| ”Permission denied” on a mount | Container’s user ≠ owner of host files | --user $(id -u):$(id -g) |
| ”Address already in use” | Host port is taken | docker ps to find the squatter, or pick another port |
| ”No space left on device” | Images/build cache filled the disk | docker system df, then docker system prune |
| Containers can’t reach each other | They’re on the default network | Put them on a network you created (Part 4) |
No shell in the container? Distroless and
scratchimages have nosh. Attach a debug toolbox that shares the container’s namespaces:docker run -it --rm --network=container:web nicolaka/netshoot.
And when the disk fills up (it will), this is your reset button:
docker system df # see what's using space
docker system prune # remove stopped containers, unused networks, dangling images
docker system prune -a --volumes # nuclear: also unused images AND volumes
7. Try it yourself
Three short exercises, public images only.
1 — Live-edit with a bind mount (5 min). Serve a site/ folder with nginx (Part 3). Edit index.html, refresh — confirm the change appears with no rebuild. Then try writing to it from inside the container (docker exec -it web sh) and watch :ro block you.
2 — Prove a volume survives (5 min). Start mongo with -v app-data:/data/db, insert a doc (docker exec -it mongo mongosh --eval 'db.t.insertOne({hi:1})'), docker rm -f mongo, start it again with the same volume, and confirm the doc is still there.
3 — Talk by name (10 min). Create app-net, run mongo on it, and from another container ping mongo — it resolves. Then run mongo with no --network and watch the same ping fail with “bad address.” That contrast is the whole networking lesson.
Where this leaves you
You can now run, inspect, and debug containers; keep data with volumes; connect services by name; and build images that cache well. That’s the working core — more than enough for everything in this course.
When you hit something this doesn’t cover, docker <cmd> --help and docs.docker.com are genuinely excellent. Reach for them as needed rather than memorizing up front.