← Back to dashboard
dockertoolingfresh

Docker (Windows) — Containers Developer Guide

What is Docker?

The real model

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.

Text
██████╗  ██████╗  ██████╗██╗  ██╗███████╗██████╗
██╔══██╗██╔═══██╗██╔════╝██║ ██╔╝██╔════╝██╔══██╗
██║  ██║██║   ██║██║     █████╔╝ █████╗  ██████╔╝
██║  ██║██║   ██║██║     ██╔═██╗ ██╔══╝  ██╔══██╗
██████╔╝╚██████╔╝╚██████╗██║  ██╗███████╗██║  ██║
╚═════╝  ╚═════╝  ╚═════╝╚═╝  ╚═╝╚══════╝╚═╝  ╚═╝

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 in docs.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

  1. Overview & architecture
  2. Install Docker Desktop on Windows
  3. The WSL 2 backend & the filesystem rule
  4. Core CLI quickstart
  5. Images & the Dockerfile
  6. Docker Compose
  7. Volumes & bind mounts (Windows gotchas)
  8. Networking & ports
  9. docker init — scaffold in one command
  10. Claude Code + Docker
  11. Troubleshooting
  12. 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


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 --version to check; wsl --update to 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:

PowerShell
# 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):

Text
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:

Bash
docker version          # client + server (engine) both report
docker run --rm hello-world

If 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. A docker build or a bind-mounted dev server reads thousands of small files; on /mnt/c every read crosses the Windows↔Linux boundary and the build crawls. From ~/code it runs at native speed.

Bash
# 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 engine

Bonus: 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):

Bash
# 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
CommandDoes
docker run [-d] [-p host:ctr] [-e K=V] IMGCreate + 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> shOpen a shell inside a running container
docker logs -f <ctr>Stream logs
docker compose up -dBring up the whole stack (see §6)
docker pull/push <ref>Pull from / push to a registry (Docker Hub)
docker system pruneReclaim 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:

Dockerfile
# 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 --from=deps /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]

# build — produce the production bundle
FROM base AS build
COPY --from=deps /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 --from=deps /app/node_modules ./node_modules
COPY --from=build /app/.next ./.next
COPY --from=build /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):

gitignore
node_modules
.git
.next
npm-debug.log
.env*
Dockerfile
.dockerignore

Layer-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):

YAML
# 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:
Bash
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 web app connects to Postgres at db:5432, not localhost. localhost inside 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:

TypeSyntaxUse for
Named volume--mount type=volume,src=dbdata,target=/var/lib/postgresql/dataDatabases, anything the engine should own
Bind mount--mount type=bind,src="$(pwd)",target=/appLive-editing source during dev
Bash
# 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 ~/code and bind from there.
  • Git Bash path mangling. In Git Bash on Windows, MSYS rewrites /app into a Windows path. Escape it with a leading double slash — -w //app and src=".//" — or just run from WSL/PowerShell where this doesn't happen. (This is why Docker's own docs show -w //app in 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.

Bash
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.1 for anything with data. -p 5432:5432 listens on all interfaces; -p 127.0.0.1:5432:5432 keeps 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:

Bash
cd ~/code/your-repo
docker init            # answers a few prompts, writes the four files
docker compose up      # run what it scaffolded

It'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.

Bash
# 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 -v to 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

SymptomFix
docker: command not found in WSLSettings → 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 slowRepo 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 allocatedAnother process owns it — change the host port (-p 3001:3000) or stop the other container
Container can't reach another serviceUse the service name (db:5432), not localhost, inside the Compose network
Disk filling updocker system df then docker system prune -af --volumes (deletes unused volumes!)
WSL VM eating RAMCap it: %UserProfile%\.wslconfig[wsl2] memory=8GB, then wsl --shutdown
Image hugeUse 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, not node:latest) so builds are reproducible.
  • Secrets never bake into images. Don't COPY .env or ENV 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, Compose env_file:, or build secrets --mount=type=secret). Keep .env* in .dockerignore and .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/c are 2–20× slower. ~/code always.
  • The whole local stack in one file. For a typical codeAmani app, a single compose.yaml runs the web app + Postgres (or a local Supabase) + Redis/Upstash-compatible cache, so a new dev is one docker compose up from a running environment. Document it in the repo's README.Docker.md (docker init writes a starter).
  • Modest hardware. Many East-African dev machines are RAM-light — cap the WSL VM ([wsl2] memory=) and prefer -alpine/-slim bases to keep images and pulls small on metered connections.