Library
00/08 · ~40 min
GUIDEDECK · for shipping software that runs anywhere

Docker & Containers
package the environment,
not just the code.

A 40-minute working session on containers — from the "works on my machine" problem, through images, Dockerfiles and layers, volumes, networking, and Compose, then the tools you'll choose between, to a real bad → good setup you can copy.

~40 MINENGINEERSTOOL-AGNOSTIC
SCROLL
01 · The problem they solve 4 min

"Works on my machine"
is an environment bug.

Code rarely fails alone — it fails because the machine around it is different: another Node version, a missing system library, a config that only exists on someone's laptop. Containers fix this by shipping the environment with the code.

A container an isolated, packaged process — bundles your app with everything it needs to run (runtime, libraries, files, env) into one unit that behaves the same on a laptop, in CI, and in production. Same image in, same behavior out.
3

environments where it must behave identically: dev, CI, prod.

1

artifact to rule them all — the image you build once.

ms

startup, not minutes — containers share the host kernel.

MBs

not GBs — no full guest OS to ship around.

Manual setup — drifts over time
# Getting started (good luck) # 1. install Node 18 (NOT 20, it breaks) # 2. brew install postgres@15, then createdb app # 3. set 7 env vars (ask Sam which ones) # 4. nvm use; npm i; ./scripts/seed.sh # …works for Sam. Not for you. Not in CI.
Containerized — reproducible
# Getting started (actually) docker compose up # the image pins the runtime, libs, and config. # identical on every laptop, in CI, in prod. # onboarding goes from a day to a minute.

VMs vs. containers

  • A virtual machine virtualizes hardware — each VM carries a full guest OS. Heavy: GBs, boots in seconds-to-minutes.
  • A container virtualizes the operating system — all containers share the host kernel (the core of the OS that talks to the hardware) and isolate only the process.
  • Result: containers are small and start instantly, so you can run many per host and rebuild them constantly.
VIRTUAL MACHINES App App App Guest OS Guest OS Guest OS Hypervisor Host OS Hardware CONTAINERS App App App + libs + libs + libs Docker Engine Host OS · shared kernel Hardware full OS per app → GBs one kernel for all → MBs

VMs virtualize hardware (a full OS each); containers virtualize the OS (one shared kernel) — so they ship in MBs and start in milliseconds.

02 · The mental model 6 min

An image is the blueprint;
a container is one running instance.

Get this one distinction right and the rest of Docker falls into place. The relationship is exactly class → object: build the template once, then run as many live copies as you like.

An image is a read-only, layered template — a snapshot of a filesystem plus metadata (the command to run, ports, env). A containeris an image that's been started: a running process with its own thin writable layer on top. One image → many containers.

The lifecycle, in commands

  • docker build turns a Dockerfile into an image.
  • docker pull downloads a prebuilt image from a registry (e.g. Docker Hub).
  • docker run starts a container from an image.
  • docker ps lists running containers; stop / rm end and delete them.
docker builddocker pulldocker rundocker psdocker stopdocker rm
myapp:1.0 app code layer npm deps layer node runtime base OS files READ-ONLY · SHARED docker run writable layer container · web-1 writable layer container · web-2

Containers share the image's read-only layers; each adds a thin writable layer of its own.

Pets — mutate the running container
docker exec -it web bash apt-get install imagemagick # fix it live… vim /app/config.json # tweak by hand # changes live only in this container. # rebuild or restart → all of it vanishes.
Cattle — change the image, redeploy
# put the change in the Dockerfile / config docker build -t myapp:1.1 . docker run myapp:1.1 # the fix is versioned, reviewable, repeatable. # any container from this image is identical.

Treat containers as disposable and immutable: never patch a running one. If you need a change, change the image and roll a new container. Cattle, not pets— you don't nurse a sick container back to health by hand; you replace it with a fresh one built from the updated image.

03 · Building images 7 min

A Dockerfile is a recipe.
Every line is a cached layer.

Docker builds an image by running your Dockerfile top-to-bottom, and it caches each instruction as a layer. Understanding that cache is the difference between a 2-second rebuild and a 5-minute one — and between a 1.2GB image and a 90MB one.

A Dockerfile is a plain-text list of build steps. Each instruction (FROM, COPY, RUN…) produces one immutable layer. Docker reuses a layer's cache as long as that step and everything above ithasn't changed — so order matters enormously.

The instructions you'll use daily

  • FROM — the base image to start from.
  • WORKDIR — set the working directory.
  • COPY — copy files from your context into the image.
  • RUN — execute a command at build time (install deps, compile).
  • EXPOSE — document the port the app listens on.
  • CMD — the default command run at start time. Write it as a list — ["node","server.js"], the exec form— so that when Docker stops the container the "please shut down" signal reaches your app directly and it can exit cleanly. The plain-string shell form (node server.js) hides your app behind a shell, so it gets killed abruptly instead.
FROM node:20-slim # small base WORKDIR /app COPY package*.json ./ # deps first… RUN npm ci # …cached unless they change COPY . . # then the source EXPOSE 3000 CMD ["node", "server.js"] # exec form

Deps copied before source: edit a route and only the last two layers rebuild.

FROM node:20-slim WORKDIR /app COPY package*.json RUN npm ci COPY . . ← edited CMD ["node"…] cached ✓ rebuilt ✕ (this layer + everything below)

A cache hit stops at the first changed layer; everything below it rebuilds. Put what changes least at the top.

The #1 Dockerfile mistake

copies source before deps
FROM node:20 # ~1GB base COPY . . # any edit busts cache RUN npm install # reruns EVERY build CMD npm start # shell form, no signals
deps cached, slim base
FROM node:20-slim COPY package*.json ./ RUN npm ci # cached on code edits COPY . . CMD ["node","server.js"]

Two techniques that pay for themselves

Multi-stage builds

Build fat, ship thin

Use one stage with the full toolchain to compile, then copy only the output into a tiny runtime image. Your compiler, dev dependencies and source never reach production.

FROM node:20 AS build WORKDIR /app COPY . . RUN npm ci && npm run build # runtime stage — only the built assets FROM nginx:alpine COPY --from=build /app/dist /usr/share/nginx/html

Like cooking in a full kitchen, then handing over just the plated dish — not the pots and pans.

.dockerignore

Keep junk out of the build

The build context is everything Docker uploads before building. A .dockerignore excludes the things that bloat images and bust the cache — and stops secrets sneaking in.

node_modules .git .env # never bake secrets into an image *.log dist Dockerfile

Like  a .gitignore for your image — smaller, faster, safer builds.

04 · Data that survives 5 min

A container's filesystem is throwaway.
Volumes are where data lives.

That thin writable layer on top of the image dies with the container. Delete the container — or redeploy a new image — and anything written inside is gone. For databases, uploads, and state, you need storage that outlives the process.

A volume is storage managed by Docker that lives outside the container's writable layer. Containers come and go; the volume — and your data — stays. Mount it into a path inside the container and writes there persist across restarts, rebuilds, and deletes.
container · db image layers (read-only) writable layer dies on docker rm ✕ volume · pgdata /var/lib/postgresql survives the container ✓ mount

Writes to a mounted path land in the volume, not the disposable writable layer — so they outlive the container.

no volume — data evaporates
docker run --name db postgres:16 # write 10,000 rows… docker rm -f db # every row is gone. forever.
named volume — data persists
docker volume create pgdata docker run --name db \ -v pgdata:/var/lib/postgresql/data \ postgres:16 # rm + recreate → rows are still there.

Three ways to mount storage

Named volume

Docker-managed

Docker owns the storage location. The default for production data — databases, uploads. Portable, backup-able, and decoupled from the host path.

-v pgdata:/var/lib/postgresql/data
Bind mount

A host folder, live

Maps a real directory on your machine into the container. Perfect for local dev — edit source on the host, the container sees it instantly (hot reload).

-v $(pwd)/src:/app/src
tmpfs

Memory only

Stored in RAM, never on disk, gone when the container stops. For secrets or scratch data you explicitly never want persisted.

--tmpfs /tmp/cache

Like a hot-desk office: the desk (container) is wiped each night, but your locker (volume) keeps your things between days.

05 · Talking to the world 5 min

Containers are isolated by default.
You publish ports and join networks to connect them.

Two questions trip everyone up at first: how does the outside reach a container? (publish a port) and how do containers reach each other? (put them on the same network and use service names). Get these and Compose feels obvious.

Publishing a port with -p HOST:CONTAINER forwards a port on your machine to one inside the container. A user-defined network is a private bridge that lets containers on it find each other by name, via Docker's built-in DNS — no IPs, no --link.
browser :8080 -p 8080:3000 user network · app-net web listens :3000 db listens :5432 connect("db:5432") ✓

Outside → in via a published port; inside → inside via the service name db on the shared network.

Reading -p 8080:3000

  • Left is the host port (what you type in the browser), right is the container port (what the app listens on).
  • localhost:8080 on your machine now reaches the app bound to :3000 inside.
  • Without -p, the port is reachable from other containers on the network but not from the host.
  • Inside a network, containers talk on the container's port directly — no publishing needed.
Hard-coded IPs & legacy --link
# container IPs change on every restart DB_HOST="172.17.0.3" # --link is deprecated and brittle docker run --link db:db web
Shared network + DNS by name
docker network create app-net docker run --network app-net --name db postgres:16 docker run --network app-net web # web connects to "db:5432" — stable forever
06 · Multi-service setups 6 min

One file. One command.
The whole stack up.

Real apps aren't one container — they're a web service, a database, maybe a cache and a queue, all wired together. Typing long docker run commands in the right order, every time, is a recipe for mistakes. Compose makes the stack declarative.

Docker Compose describes a multi-container application in one YAML file. You declare the services, their images, ports, volumes, env and dependencies; docker compose up creates the network and starts everything. The file is the documentation.
services: web: build: . ports: ["8080:3000"] environment: DATABASE_URL: postgres://db:5432/app depends_on: db: { condition: service_healthy } db: image: postgres:16 volumes: ["pgdata:/var/lib/postgresql/data"] healthcheck: test: ["CMD", "pg_isready", "-U", "postgres"] volumes: pgdata:

Everything from sections 2–5 in one place: image, port, env, volume, dependency order.

What this one file gives you

  • A private network is created automatically — web reaches db by name, no config.
  • depends_on + healthcheck start the DB and wait until it's actually ready before web.
  • The named volume keeps Postgres data across down/up cycles.
  • build: . builds web from your Dockerfile; image: pulls db prebuilt.
compose up -dcompose downcompose logs -fcompose pscompose build
Imperative — order & flags by hand
docker network create app-net docker volume create pgdata docker run -d --name db --network app-net \ -v pgdata:/var/lib/postgresql/data postgres:16 # now sleep and hope the db is ready… docker run -d --name web --network app-net \ -p 8080:3000 -e DATABASE_URL=... myapp
Declarative — the file is the truth
docker compose up -d # network, volume, build, healthcheck-gated # startup order — all from compose.yaml. # tear it all down with one command: docker compose down
  • Compose is for one host — local dev, CI, simple single-box deploys. It is fantastic at exactly that.
  • When you need to run across many machines, with rolling deploys, self-healing and autoscaling, you graduate to an orchestrator — Kubernetes (or Nomad / ECS).
  • The mental model carries over: services, images, networks and volumes are the same nouns, just scheduled across a cluster.
07 · Pick the right tool 4 min

The container world is
bigger than Docker.

Docker popularized containers, but at each step — where you store images, what you build them with, which base you start from, and how you run them at scale — there are real alternatives. Here are the leading ones, with one honest pro and con each, and a quick rule for choosing.

Four decisions, four sets of tools. A registry is the cloud library you push images to and pull them from (like npm or GitHub, but for images). A build engine is the program that turns your Dockerfile into an image. A base image is the starting FROM layer everything else stacks on. An orchestrator runs your containers across one or many machines.
Dockerfile imagebuild engine registryHub · GHCR · ECR runserver / CI build push pull

The pipeline shared by every tool below: build an image, push it to a registry, pull and run it elsewhere. The vendors just swap in at each arrow.

How to read these tabs

  • Each card names a real, widely-used option with one genuine strength and one genuine trade-off.
  • The Pro / Con pair is deliberately blunt — every tool here is a reasonable default for some team.
  • Read the Best for line at the bottom of each tab to skip straight to the choice that fits you.

Where your images live

Docker Hub

The default library

Pro — the biggest public catalog and where docker pull looks first, so almost every base image is one command away.

Con— free accounts hit strict pull rate limits, so busy CI pipelines can fail with "too many requests."

GitHub · GHCR

Next to your code

Pro — free for public images and sits right beside your repo and GitHub Actions, so building and publishing share one set of permissions.

Con— access is tied to GitHub accounts and teams, and it's less of a public discovery hub than Hub.

AWS · ECR

Private, in your cloud

Pro — fast and private, and wired tightly into AWS permissions and runners (ECS / EKS), so deploys stay inside one account.

Con — AWS-only, billed per GB stored, and logging in needs short-lived tokens and the right region set up.

Best for  public images and learning → Hub; you already live on GitHub → GHCR; you run on AWS and want everything private → ECR.

What turns a Dockerfile into an image

Docker

The standard

Pro — what everyone knows, with the largest ecosystem; its modern builder (BuildKit) gives fast, well-cached builds out of the box.

Con — it runs a background service as the root user, and Docker Desktop needs a paid license at larger companies.

Podman · Buildah

Daemonless & rootless

Pro — runs without a background service and without root by default (safer), and its CLI mirrors docker almost command-for-command.

Con — a smaller ecosystem; some Compose and Desktop conveniences lag behind or behave slightly differently.

Kaniko · Buildpacks

Build in CI, no daemon

Pro — build images inside CI or a Kubernetes cluster with no Docker daemon at all; Buildpacks can even skip the Dockerfile entirely.

Con — less flexible than a hand-written Dockerfile, and each adds its own concepts to learn.

Best for  most people → Docker; security or no-root environments → Podman; building inside a cluster or pipeline → Kaniko / Buildpacks.

The FROM you start with

Full · node:20

Batteries included

Pro— every shell and tool is already there, so it's the easiest to poke around in and debug.

Con — often 1GB+ and a bigger attack surface (more installed software means more things that could have security holes).

Slim · Alpine

Small and fast

Pro — tens of MB instead of a gigabyte, so images pull and start noticeably faster.

Con — Alpine uses a different system C library (musl, not the usual glibc), which can break native add-ons; fewer built-in debug tools.

Distroless

App and nothing else

Pro — ships only your app and its runtime — no shell, no package manager — so the attack surface is about as small as it gets.

Con — no shell to exec into makes debugging harder; you must build elsewhere and copy in (multi-stage).

Best for  quick local hacking → full; everyday production → slim; locked-down production → distroless.

Running containers at scale

Docker Compose

One box, one file

Pro — dead simple: one YAML file brings the whole stack up. Perfect for local dev, CI, and small single-server deploys.

Con — it only manages one machine, with no automatic restart-on-failure, autoscaling, or zero-downtime rolling updates.

Kubernetes

The cluster standard

Pro — the industry default for many machines: it restarts crashed containers, scales them up and down, and rolls out new versions with no downtime.

Con — a steep learning curve and real day-to-day operational work; genuinely overkill for a small app.

ECS · Cloud Run · Nomad

Managed middle ground

Pro — the cloud runs the hard control plane for you, so you get scaling and self-healing with far less to operate than raw Kubernetes.

Con — more tied to one vendor and less portable, with fewer knobs than running Kubernetes yourself.

Best for  dev and one server → Compose; many servers and a platform team → Kubernetes; scale without the ops burden → a managed service.

The honest default for most teams: Docker to build, a slim base, your platform's registry, and Composeuntil one machine genuinely isn't enough. Reach for the heavier options only when a real need shows up.

08 · A real setup & recap 3 min

From a fragile image
to one you'd ship.

Everything in one place: the same web service, done the way that bites you versus the way that holds up. Then five rules to walk out with.

What goes wrong

FROM node:20 # 1GB+ base COPY . . # node_modules, .git, .env and all RUN npm install # reruns on every code change EXPOSE 3000 CMD npm start # shell form → no graceful shutdown # runs as root · secrets baked in · 1.3GB image · slow builds
Symptoms
Minutes-long rebuilds, huge images, leaked .env, and a process that ignores SIGTERM on deploy.
Root cause
Cache-busting layer order, no .dockerignore, fat base, shell-form CMD.

What good looks like

FROM node:20-slim AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM node:20-slim # clean runtime stage WORKDIR /app COPY --from=build /app/dist ./dist COPY --from=build /app/node_modules ./node_modules USER node # drop root EXPOSE 3000 CMD ["node", "dist/server.js"] # exec form → clean signals
Wins
Cached deps, slim multi-stage image, non-root user (so a compromised app can't act as the all-powerful root account), exec-form CMD, secrets kept out via .dockerignore.
Pair it with
A compose.yaml for the db + volume + network, so compose up brings the whole stack online.

Five rules to walk out with

1Ship the environment, not instructions. The image is the artifact — identical in dev, CI and prod.
2Image = blueprint, container = instance. Treat containers as disposable; change the image, never patch a running one.
3Order layers by how often they change. Deps before source, slim bases, multi-stage builds, a .dockerignore.
4State lives in volumes.The writable layer is throwaway — anything you can't afford to lose gets a volume.
5One network, names not IPs; one Compose file. Declare the stack and bring it up with a single command.

Keep going

  • docs.docker.com— the official guides & Dockerfile reference
  • Dockerfile best practices— the build-cache & image-size checklist
  • Play with Docker — a free in-browser sandbox to try it live
  • Kubernetes— the next step when one host isn't enough

One sentence to remember

"Build once, run anywhere — because the environment ships with the code."

— the whole point of containers

Knowledge check

Did it stick?

Five quick questions on images, layers, volumes, networking and Compose — instant feedback, no sign-in.

Rate this deck
be the first

Navigate with ← → or scroll · back to library