learn phase 1 session 5 Deep Dive

Session 5 — Deep Dive: Docker in Practice

Read this after the main session 5 handout. This isn’t a command catalog — docker --help already 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.

CommandWhat it’s forExample
docker runCreate and start a containerdocker run -d --name web -p 8080:80 nginx:alpine
docker psWhat’s running (-a includes stopped)docker ps -a
docker logsSee its output (-f to follow live)docker logs -f web
docker execGet a shell / run a command insidedocker exec -it web sh
docker inspectFull config as JSON (pipe to jq)docker inspect web | jq '.[0].State'
docker stopGraceful stop (SIGTERM, then SIGKILL)docker stop web
docker rmDelete a container (-f forces)docker rm -f web
docker buildBuild an image from a Dockerfiledocker build -t myapp:1.0 .
docker imagesList local imagesdocker images
docker pullDownload an imagedocker 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:

FlagWhat it doesExample
-dRun in the backgrounddocker run -d nginx:alpine
--nameGive it a name (else you get silly_morse)docker run -d --name web nginx:alpine
--rmAuto-delete when it exits (great for one-offs)docker run --rm alpine echo hi
-itInteractive shelldocker run -it --rm alpine sh
-p <host>:<container>Publish a portdocker run -p 8080:80 nginx:alpine
-e / --env-filePass config indocker run -e LOG_LEVEL=debug alpine env
-vMount a volume or host folder (Part 3)docker run -v data:/data mongo:7
--restartAuto-restart policydocker run -d --restart unless-stopped nginx:alpine
-m / --cpusCap memory / CPUdocker run -m 256m --cpus 0.5 nginx:alpine
--health-cmdTell Docker how to check it’s alive`—health-cmd ‘wget -qO- localhost/

Bind to localhost in dev. -p 8080:80 listens on all interfaces — anyone on your network can hit it. Use -p 127.0.0.1:8080:80 so 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 — keep node_modules, .git, build output out of the build context. Smaller context = faster builds and smaller images.
  • ARG is not a secret. Build args show up in docker 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:

SymptomLikely causeFix
Exits immediately with code 0Nothing keeps it alive — the command finishedRun a long-lived process; for one-offs use --rm
Restart loopThe startup command failsdocker logs — usually a missing env var
”Permission denied” on a mountContainer’s user ≠ owner of host files--user $(id -u):$(id -g)
”Address already in use”Host port is takendocker ps to find the squatter, or pick another port
”No space left on device”Images/build cache filled the diskdocker system df, then docker system prune
Containers can’t reach each otherThey’re on the default networkPut them on a network you created (Part 4)

No shell in the container? Distroless and scratch images have no sh. 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.