Docker (Windows) — Containers Developer Guide
What is Docker?
Docker Desktop + WSL 2 backend = the prod Linux runtime on your Windows laptop.
Build lean images with multi-stage Dockerfiles (compile in a fat stage, `COPY --from` only artifacts into a non-root `-alpine` runner) and order layers least- to most-frequently-changed so source edits don't bust the `npm ci` cache. `docker compose up` / `compose watch` runs app + Postgres + cache locally; services talk by name (`db:5432`), not localhost. The codeAmani payoff is parity — the local image is the Linux image you ship to Cloud Run/Render — and the one rule that governs build speed mirrors WSL's: keep the repo on the Linux fs (`~/code`), never `/mnt/c`, or every bind mount and build context crosses the OS boundary and crawls. Secrets pass at run time (`--env-file`), never baked into a cached, shippable layer.
Six Docker building blocks
An image is the frozen template; a container is it running. The rest is how you build, compose, and persist.
██████╗ ██████╗ ██████╗██╗ ██╗███████╗██████╗
██╔══██╗██╔═══██╗██╔════╝██║ ██╔╝██╔════╝██╔══██╗
██║ ██║██║ ██║██║ █████╔╝ █████╗ ██████╔╝
██║ ██║██║ ██║██║ ██╔═██╗ ██╔══╝ ██╔══██╗
██████╔╝╚██████╔╝╚██████╗██║ ██╗███████╗██║ ██║
╚═════╝ ╚═════╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝Docker (Windows) — Containers Developer Guide
Focus: Everything a developer needs to run Docker on Windows 11 — install Docker Desktop on the WSL 2 backend, the filesystem rule that decides your build speed, the core CLI, writing a multi-stage Dockerfile, Docker Compose, volumes & bind mounts (incl. the Windows path gotchas),
docker init, and driving it all with Claude Code inside WSL. Grounded indocs.docker.com; reviewed 2026-06-16.
Containers solve "works on my machine" by shipping the app and its environment as one immutable image. On Windows the whole thing rides on WSL 2 — Docker Desktop runs the Linux engine inside the same lightweight VM WSL uses, so the images you build locally are byte-for-byte the Linux images you deploy. Get the install + the one filesystem rule right and you have production parity on your laptop. Let's dive in.
Table of Contents
- Overview & architecture
- Install Docker Desktop on Windows
- The WSL 2 backend & the filesystem rule
- Core CLI quickstart
- Images & the Dockerfile
- Docker Compose
- Volumes & bind mounts (Windows gotchas)
- Networking & ports
docker init— scaffold in one command- Claude Code + Docker
- Troubleshooting
- codeAmani notes
The interactive learn module above this page is a live container-vs-image + Dockerfile-layer explainer — start there for intuition, then use this reference.
Official Documentation
| Resource | URL |
|---|---|
| Install Docker Desktop on Windows | https://docs.docker.com/desktop/setup/install/windows-install/ |
| Docker Desktop WSL 2 backend | https://docs.docker.com/desktop/features/wsl/ |
| Get started / workshop | https://docs.docker.com/get-started/ |
docker CLI reference | https://docs.docker.com/reference/cli/docker/ |
| Compose (overview + file ref) | https://docs.docker.com/compose/ |
| Dockerfile reference | https://docs.docker.com/reference/dockerfile/ |
| Build best practices | https://docs.docker.com/build/building/best-practices/ |
1. Overview & architecture
Two words decide everything: image and container.
- An image is a read-only, layered template — your app, its runtime, and its dependencies, frozen. Built from a
Dockerfile. - A container is a running instance of an image — an isolated process with its own filesystem, network, and PID space. You can run many containers from one image.
On Windows, the Docker engine (the daemon that builds images and runs containers) does not run on Windows directly — it runs inside the WSL 2 Linux VM. Docker Desktop is the control plane (GUI, settings, the docker CLI shim) that talks to that engine. Here's the whole stack:
Why this matters: the containers run on a genuine Linux kernel — the same kernel family as your production hosts (Cloud Run, Render, a Linux VM). There is no translation layer faking Linux; an image that runs here runs there. That's the dev/prod parity payoff.
2. Install Docker Desktop on Windows
System requirements (WSL 2 backend):
- WSL version 2.1.5 or later (
wsl --versionto check;wsl --updateto upgrade) - Windows 11 64-bit: Enterprise/Pro/Education 23H2 (build 22631) or higher — or Windows 10 64-bit 22H2 (build 19045)
- 64-bit processor with SLAT, 8 GB RAM, and hardware virtualization enabled in BIOS/UEFI
Install — download Docker Desktop Installer.exe from docs.docker.com/desktop/setup/install/windows-install/, then either double-click it or run from a terminal:
# All-users install (run the terminal as Administrator)
Start-Process -Wait -FilePath ".\Docker Desktop Installer.exe" -ArgumentList "install"
# Per-user install (no admin) — installs only for the current user
Start-Process -Wait -FilePath ".\Docker Desktop Installer.exe" -ArgumentList "install","--user"The installer enables the WSL 2 feature for you if it's missing. After install, launch Docker Desktop once and accept the service agreement. The whale icon in the system tray = engine running.
Turn on the WSL 2 engine + per-distro integration (usually on by default):
Docker Desktop → Settings
→ General → ✅ Use WSL 2 based engine
→ Resources → WSL integration → ✅ Enable integration with my default WSL distro
→ ✅ <your distro, e.g. Ubuntu>Then, inside your WSL distro, confirm the CLI is wired up:
docker version # client + server (engine) both report
docker run --rm hello-worldIf a distro is still on WSL 1, convert it: wsl --set-version <distro> 2.
3. The WSL 2 backend & the filesystem rule
Docker on Windows inherits WSL's #1 performance rule — for the same reason (the OS boundary). See the WSL guide for the full story.
✅ Keep your repo in the Linux filesystem (
/home/you/code/...), not/mnt/c. Adocker buildor a bind-mounted dev server reads thousands of small files; on/mnt/cevery read crosses the Windows↔Linux boundary and the build crawls. From~/codeit runs at native speed.
# Right: clone into the Linux fs, build from there
mkdir -p ~/code && cd ~/code
git clone https://github.com/codeamani-solutions/your-repo.git
cd your-repo
docker build -t your-repo . # fast — files are local to the engineBonus: WSL 2 lets multiple distros share one Docker engine, and Docker Desktop manages the VM's resources for you (caps live in %UserProfile%\.wslconfig, e.g. [wsl2] memory=8GB).
4. Core CLI quickstart
The verbs you'll use every day. Run them from inside WSL (or PowerShell — both reach the same engine):
# Images
docker pull node:22-alpine # fetch an image from Docker Hub
docker images # list local images
docker build -t myapp:dev . # build an image from ./Dockerfile, tag it
# Containers
docker run -d --name web -p 3000:3000 myapp:dev # run detached, publish a port
docker ps # running containers (-a = include stopped)
docker logs -f web # tail a container's logs
docker exec -it web sh # shell into a running container
docker stop web && docker rm web # stop + remove
# Housekeeping
docker system df # disk used by images/containers/volumes
docker system prune -f # reclaim space (dangling images, stopped ctrs)
docker system prune -af --volumes # aggressive: also unused images + volumes| Command | Does |
|---|---|
docker run [-d] [-p host:ctr] [-e K=V] IMG | Create + start a container |
docker ps [-a] | List running (or all) containers |
docker build -t name:tag . | Build an image from the Dockerfile in . |
docker exec -it <ctr> sh | Open a shell inside a running container |
docker logs -f <ctr> | Stream logs |
docker compose up -d | Bring up the whole stack (see §6) |
docker pull/push <ref> | Pull from / push to a registry (Docker Hub) |
docker system prune | Reclaim disk from unused objects |
5. Images & the Dockerfile
A Dockerfile is the recipe. The big lever for small, fast, secure images is multi-stage builds: compile in a fat stage, copy only the artifacts into a lean final stage. Here's a production-grade Next.js example:
# syntax=docker/dockerfile:1
FROM node:22-alpine AS base
WORKDIR /app
# deps — install once, cache by lockfile
FROM base AS deps
COPY package*.json ./
RUN npm ci
# dev — hot-reload target used by Compose in development
FROM base AS dev
ENV NODE_ENV=development
COPY /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
# build — produce the production bundle
FROM base AS build
COPY /app/node_modules ./node_modules
COPY . .
RUN npm run build
# runner — lean, non-root, only the built output
FROM base AS runner
ENV NODE_ENV=production
COPY /app/node_modules ./node_modules
COPY /app/.next ./.next
COPY /app/public ./public
EXPOSE 3000
USER node
CMD ["npm", "start"]Always pair it with a .dockerignore so junk never enters the build context (faster builds, smaller images, fewer secret leaks):
node_modules
.git
.next
npm-debug.log
.env*
Dockerfile
.dockerignoreLayer-caching rule of thumb: order from least- to most-frequently-changed. Copy package*.json and npm ci before COPY . ., so editing source code doesn't bust the dependency layer.
6. Docker Compose
Compose declares a multi-container stack in one compose.yaml and brings it up with a single command — perfect for "app + Postgres + Redis" local dev. The target: line ties a service to a Dockerfile stage (§5):
# compose.yaml
services:
web:
build:
context: .
target: dev # use the hot-reload stage from the Dockerfile
ports:
- "3000:3000"
env_file:
- .env.local # never committed — see codeAmani notes
depends_on:
- db
develop:
watch: # rebuild/sync on file changes
- action: sync
path: .
target: /app
- action: rebuild
path: package.json
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
volumes:
- dbdata:/var/lib/postgresql/data
ports:
- "5432:5432"
volumes:
dbdata:docker compose up -d # build + start the stack in the background
docker compose watch # live-sync/rebuild as you edit (modern dev loop)
docker compose logs -f web # tail one service
docker compose ps # what's running
docker compose down # stop + remove containers + network
docker compose down -v # ...and delete named volumes (wipes the DB)Inside the Compose network, services reach each other by service name — the
webapp connects to Postgres atdb:5432, notlocalhost.localhostinside a container is the container itself.
7. Volumes & bind mounts (Windows gotchas)
Containers are ephemeral — their writable layer dies with them. Two ways to persist or share data:
| Type | Syntax | Use for |
|---|---|---|
| Named volume | --mount type=volume,src=dbdata,target=/var/lib/postgresql/data | Databases, anything the engine should own |
| Bind mount | --mount type=bind,src="$(pwd)",target=/app | Live-editing source during dev |
# Dev loop: bind-mount the source so edits reflect instantly
docker run -dp 127.0.0.1:3000:3000 \
-w /app --mount type=bind,src="$(pwd)",target=/app \
node:22-alpine sh -c "npm install && npm run dev"Windows-specific gotchas:
- Bind-mount the Linux fs, not
/mnt/c. A bind mount from/mnt/c/...is slow (the §3 boundary) and loses Linux file metadata. Keep the repo in~/codeand bind from there. - Git Bash path mangling. In Git Bash on Windows, MSYS rewrites
/appinto a Windows path. Escape it with a leading double slash —-w //appandsrc=".//"— or just run from WSL/PowerShell where this doesn't happen. (This is why Docker's own docs show-w //appin the Git Bash examples.) - File watching. Hot-reload (Next.js/Vite) on a bind-mounted Windows path can miss change events; Compose's
develop.watch(§6) is the reliable modern alternative.
8. Networking & ports
-p host:container publishes a container port to the host. With Docker Desktop's WSL 2 backend, published ports are reachable at localhost from both Windows and WSL — so a container on -p 3000:3000 opens in your Windows browser at http://localhost:3000.
docker run -d -p 8080:80 nginx # nginx :80 → http://localhost:8080
docker run -d -p 127.0.0.1:5432:5432 postgres:16 # bind to loopback only (safer)Bind to
127.0.0.1for anything with data.-p 5432:5432listens on all interfaces;-p 127.0.0.1:5432:5432keeps your dev Postgres off the LAN. Compose services talk over their private network by name (db:5432) and only need a published port when you (the host) connect.
9. docker init — scaffold in one command
Don't hand-write the first Dockerfile. docker init detects your stack (Node, Python, Go, Rust, PHP, …) and generates a sensible Dockerfile, compose.yaml, .dockerignore, and README.Docker.md:
cd ~/code/your-repo
docker init # answers a few prompts, writes the four files
docker compose up # run what it scaffoldedIt's the fastest way to a working baseline; then tune the multi-stage Dockerfile (§5) and Compose file (§6) to taste.
10. Claude Code + Docker
Docker pairs naturally with running Claude Code inside WSL — same Linux toolchain, same engine.
# Inside WSL, in your repo on the Linux fs
claude
# Then, in the session:
# "Add a multi-stage Dockerfile + compose.yaml for this Next.js app"
# "Why is my docker build slow?" → it'll spot a /mnt/c repo or a missing .dockerignore
# "docker compose up and verify the app serves on :3000"Why it clicks:
- The CLI is native.
docker,docker compose, and the build cache all live in the Linux VM Claude Code is already running in — no Windows path translation. - Reproducible verification. Claude can spin a throwaway container to run tests/builds in a clean environment, then
docker compose down -vto reset — no pollution of your host. - Parity with prod. The image Claude helps you build is the artifact you deploy; "passes locally" means "passes the same Linux runtime in prod."
Let Claude run builds in containers when a task needs a clean room, but keep the repo on
~/code(the §3 rule) so the build context is fast.
11. Troubleshooting
| Symptom | Fix |
|---|---|
docker: command not found in WSL | Settings → Resources → WSL integration → enable your distro; reopen the shell |
| Engine won't start / "Docker Desktop stopped" | Confirm virtualization is on in BIOS; wsl --update; restart Docker Desktop |
docker build is painfully slow | Repo is on /mnt/c — move it to ~/code; add a .dockerignore |
| Bind mount empty / not updating (Git Bash) | Use -w //app (double slash) or run from WSL/PowerShell; for hot-reload use compose watch |
| Port already allocated | Another process owns it — change the host port (-p 3001:3000) or stop the other container |
| Container can't reach another service | Use the service name (db:5432), not localhost, inside the Compose network |
| Disk filling up | docker system df then docker system prune -af --volumes (deletes unused volumes!) |
| WSL VM eating RAM | Cap it: %UserProfile%\.wslconfig → [wsl2] memory=8GB, then wsl --shutdown |
| Image huge | Use a multi-stage build + -alpine/-slim base; copy only build output into the runner stage |
12. codeAmani notes
- Dev/prod parity is the point. Our services deploy to Linux (Cloud Run, Render, Vercel functions are Linux too). Building on Docker Desktop's WSL 2 backend means the local image is the Linux image we ship — "works on my machine" finally means "works in prod." Pin base image tags (
node:22-alpine, notnode:latest) so builds are reproducible. - Secrets never bake into images. Don't
COPY .envorENV STRIPE_SECRET_KEY=...into a layer — image layers are cached and shippable, so a baked secret leaks. Pass secrets at run time (--env-file .env.local, Composeenv_file:, or build secrets--mount=type=secret). Keep.env*in.dockerignoreand.gitignore. This matches the stripe and supabase "server-side only" rule — Stripe signing secrets, Supabase service keys, and (for Kenya-targeted projects) Daraja/M-Pesa credentials all stay out of the image. - Run as non-root. Add
USER node(or a created user) in the final stage. A container breakout from a root process is far worse than from an unprivileged one — cheap defense, always worth it. - Keep the repo on the Linux fs. The §3 rule is not optional for Docker: build context and bind mounts on
/mnt/care 2–20× slower.~/codealways. - The whole local stack in one file. For a typical codeAmani app, a single
compose.yamlruns the web app + Postgres (or a local Supabase) + Redis/Upstash-compatible cache, so a new dev is onedocker compose upfrom a running environment. Document it in the repo'sREADME.Docker.md(docker initwrites a starter). - Modest hardware. Many East-African dev machines are RAM-light — cap the WSL VM (
[wsl2] memory=) and prefer-alpine/-slimbases to keep images and pulls small on metered connections.