Docker (Windows) — Containers Developer Guide
Technology: docker · Category: tooling · Last reviewed: 2026-08-30
Source: https://tech-stack.codeamanilabs.org/guide/docker
Insight:
On Windows, "Docker" means Docker Desktop running on the WSL 2 backend — the Linux containers you ship to Cloud Run / Render build against a real Linux kernel (the same one WSL2 runs), so dev/prod parity is built in. The single rule that governs your speed mirrors WSL's: keep the project on the Linux filesystem (
~/code, not/mnt/c). A bind-mounted build on/mnt/ccrosses the OS boundary on every file op and crawls; the same build from~/coderuns at native speed. Enable WSL integration once, run thedockerCLI from inside your distro, and you have the exact container toolchain production uses. In 2026 Docker is also an AI-native platform: hardened base images that ship with SLSA L3 provenance, a local model runtime, a containerised MCP catalog, and microVM sandboxes that run Claude Code with no access to your host.
██████╗ ██████╗ ██████╗██╗ ██╗███████╗██████╗
██╔══██╗██╔═══██╗██╔════╝██║ ██╔╝██╔════╝██╔══██╗
██║ ██║██║ ██║██║ █████╔╝ █████╗ ██████╔╝
██║ ██║██║ ██║██║ ██╔═██╗ ██╔══╝ ██╔══██╗
██████╔╝╚██████╔╝╚██████╗██║ ██╗███████╗██║ ██║
╚═════╝ ╚═════╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝
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, base-image choice (Hub rate limits vs Docker Hardened Images), scanning withdocker scout, Docker's AI stack (Model Runner, MCP Toolkit, Offload), Docker Sandboxes for running coding agents isolated, and driving it all with Claude Code inside WSL. Grounded indocs.docker.com; reviewed 2026-08-30 against Docker Engine 29.7.2 (2026-08-05) / Docker Desktop 4.88.1 (2026-08-25) / Compose v5.5.0 (2026-08-17).
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- Base images: Hub limits & Docker Hardened Images
- Scan images for CVEs —
docker scout - Docker's AI stack — Model Runner, MCP Toolkit, Offload
- Docker Sandboxes — run coding agents isolated
- 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/ |
| BuildKit (the default builder) | https://docs.docker.com/build/buildkit/ |
| Docker Scout (image CVE scanning) | https://docs.docker.com/scout/ |
| Docker Hardened Images (DHI) | https://docs.docker.com/dhi/ |
| Docker Model Runner | https://docs.docker.com/ai/model-runner/ |
| MCP Catalog & Toolkit | https://docs.docker.com/ai/mcp-catalog-and-toolkit/ |
Docker Sandboxes (sbx) |
https://docs.docker.com/ai/sandboxes/ |
| Docker Hub usage & rate limits | https://docs.docker.com/docker-hub/usage/ |
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:
flowchart TB
subgraph WIN["Windows host"]
DD["Docker Desktop · GUI + settings"]
CLI["docker CLI (PowerShell / WSL)"]
end
subgraph VM["WSL 2 · lightweight utility VM · real Linux kernel"]
ENG["dockerd — the engine"]
subgraph CTRS["Containers"]
C1["web :3000"]
C2["postgres :5432"]
C3["redis :6379"]
end
ENG --> C1
ENG --> C2
ENG --> C3
end
DD -->|manages| ENG
CLI -->|API| ENG
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) — 2.6+ if you want Enhanced Container Isolation - 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
- The Windows Server service (
LanmanServer) enabled with start mode Automatic (Docker Desktop uses it for file sharing)
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-world
If a distro is still on WSL 1, convert it: wsl --set-version <distro> 2.
Keep it current. Docker Desktop is on a fast cadence (4.88.1, 2026-08-25 at the time of review) and Engine patches carry real CVE fixes — 29.7.0 shipped a fix for CVE-2026-17106, and 29.6.x cleared a set of BuildKit findings including a command-injection issue in git checkout. Update from the GUI, or:
docker desktop update check
docker desktop update install
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.
flowchart LR
LFS["Linux fs: ~/code · FAST"]
MNT["/mnt/c: Windows C: · slow across boundary"]
B["docker build / bind mount"]
B -->|"from ~/code"| LFS
B -->|"from /mnt/c — 2–20× slower I/O"| MNT
✅ 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 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):
# 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 --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
# HEALTHCHECK lets the engine (and Compose depends_on: condition) know the app is live.
# busybox wget ships in -alpine; no extra package needed.
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget -qO- http://127.0.0.1:3000/ || exit 1
USER node
CMD ["npm", "start"]
BuildKit is the default builder for Docker Desktop and Docker Engine — the
# syntax=docker/dockerfile:1line opts into its latest frontend, enabling parallel stages, cache mounts (RUN --mount=type=cache), and build secrets (--mount=type=secret, §12).docker buildxis the extended build CLI on top of BuildKit for multi-platform (--platform linux/amd64,linux/arm64) and named builders. (Only Windows containers fall back to the legacy builder — not relevant here, since the WSL 2 backend builds Linux images.)
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
.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.
flowchart LR
A["FROM node:22-alpine"] --> B["COPY package*.json"]
B --> C["RUN npm ci ← cached unless lockfile changes"]
C --> D["COPY . . ← busts on any source edit"]
D --> E["RUN npm run build"]
BuildKit flags worth knowing
The # syntax=docker/dockerfile:1 line pins the latest stable frontend, so these
are available without extra config:
| Flag | Since | What it buys you |
|---|---|---|
RUN --mount=type=cache,target=... |
v1.2 | Persist a package-manager cache across builds — npm/pip/apt stop re-downloading |
RUN --mount=type=secret,id=... |
v1.2 | Read a secret during build without baking it into a layer |
RUN --mount=type=bind,from=... |
v1.2 | Read files from another stage/context without a COPY layer |
RUN --mount=type=ssh |
v1.2 | Use the host SSH agent for private-repo git clone |
ADD --checksum=sha256:... |
v1.6 | Verify a remote download — pin it or don't trust it |
COPY --exclude=... |
v1.19 | Skip paths inside a COPY without touching .dockerignore |
COPY --parents |
v1.20 | Preserve the source directory structure when copying globs |
# syntax=docker/dockerfile:1
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
# Cache mount: node_modules downloads survive between builds; the cache is NOT a layer.
RUN --mount=type=cache,target=/root/.npm npm ci
FROM deps AS build
COPY . .
# Build secret: available at /run/secrets/npm_token for THIS instruction only.
# Nothing is written to the image, so nothing leaks when the image is pushed.
RUN --mount=type=secret,id=npm_token NPM_TOKEN=$(cat /run/secrets/npm_token) npm run build
# Pass the secret from a file or an env var — never as a build ARG.
docker build --secret id=npm_token,src=./npm_token.txt -t myapp:dev .
docker build --secret id=npm_token,env=NPM_TOKEN -t myapp:dev .
ARGandENVare not secret. Both are recorded in the image's build history —docker historyprints them back. A token passed as--build-argis a published token. Use--mount=type=secretfor build-time credentials, and--env-file(§7) for run-time ones.
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):
No top-level
version:key. It's obsolete — the Compose spec treats it as informational only and warns if you use it (docker composealways validates against the latest schema). Start the file atservices:.docker initand the examples here already omit it; don't add it back.
# compose.yaml ← no `version:` key (obsolete)
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:
condition: service_healthy # wait for Postgres to pass its healthcheck
pre_start: # init containers — run to completion BEFORE web starts
- command: ["npm", "run", "db:migrate"]
develop:
watch: # rebuild/sync on file changes
- action: sync
path: .
target: /app
initial_sync: true # seed the container before watching
ignore:
- node_modules/
- action: rebuild
path: package.json
db:
image: postgres:17-alpine
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
volumes:
- dbdata:/var/lib/postgresql/data
ports:
- "127.0.0.1:5432:5432" # loopback only — keep the dev DB off the LAN
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
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.
docker compose(space), notdocker-compose(hyphen). Compose is now a Docker CLI plugin (thedocker composesubcommand, currently v5.5.0, bundled with Docker Desktop). The old standalone Pythondocker-composev1 reached end of life in 2024 and was removed in 2025 — if a script still calls the hyphenated form it's running unmaintained software. Convertdocker-compose …→docker compose ….
Init containers: pre_start
pre_start runs one or more ephemeral containers to completion before the
service's own container starts — and only after its depends_on conditions are
satisfied. That is exactly the shape of "migrate the database, then boot the app",
which previously needed an entrypoint wrapper or a hand-rolled wait-for-it script:
services:
web:
build: .
depends_on:
db:
condition: service_healthy
pre_start:
- command: ["npm", "run", "db:migrate"] # runs in the service's own image
- image: busybox # ...or a different one
command: sh -c 'chown -R 1000:1000 /data'
volumes:
- data:/data
There is a matching post_start (and pre_stop), but those run inside the
already-running container, not as separate ephemeral ones — use pre_start for
anything that must finish before the app accepts traffic.
Ordering trap:
depends_on: condition: service_startedonly waits for the container to exist. A Postgres container exists long before it accepts connections. Usecondition: service_healthywith a realhealthcheck:(as in the file above) or your migration step races the database on a cold start.
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:17 # 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 scaffolded
Supported platforms: ASP.NET Core, Go, Java (Maven/uber-jar), Node, PHP with Apache, Python, Rust, plus an Other general-purpose template.
It's the fastest way to a working baseline; then tune the multi-stage Dockerfile (§5) and Compose file (§6) to taste.
10. Base images: Hub limits & Docker Hardened Images
Your base image decides two things you feel later: how many CVEs you inherit on day one, and whether CI can even pull it.
Docker Hub pull rate limits
Pulls are metered, and the anonymous tier is small enough that one busy CI runner blows through it:
| Who is pulling | Limit (per 6 hours) |
|---|---|
| Unauthenticated | 100 — per IPv4 address or IPv6 /64 subnet |
| Authenticated personal account (free) | 200 |
| Pro / Team / Business | Unlimited |
The trap is the shared address: every anonymous pull from one cloud CI runner or
one office NAT draws on the same 100. A 429 Too Many Requests in the middle of a
build is almost always this, not a Docker outage. Authenticate in CI and the
problem disappears:
echo "$DOCKERHUB_TOKEN" | docker login -u "$DOCKERHUB_USER" --password-stdin
Docker itself needs no application credentials — the only variables are the optional registry logins, set as CI secrets (GitHub Actions / Vercel), never committed:
DOCKERHUB_USER=your-docker-id
DOCKERHUB_TOKEN=dckr_pat_... # a read-only access token, NOT your password
Use a scoped access token, not your account password: tokens are revocable individually and can be read-only, which is all a CI pull needs.
Docker Hardened Images (dhi.io)
Docker Hardened Images are minimal, production-ready images maintained by Docker
and published to their own registry, dhi.io. The catalog is free for community
use under Apache 2.0; paid tiers add SLA-backed patching and FIPS/STIG/ELS
variants. What you get per image:
- Near-zero known CVEs, continuously scanned and rebuilt
- Distroless variants that strip the shell and package manager — Docker measures up to a 95% smaller attack surface
- A signed SBOM and VEX statements (so a scanner can tell "vulnerable" from "not exploitable here")
- SLSA Build Level 3 provenance, cryptographically signed, on every image
That last line is why this matters to us specifically: codeAmani's supply-chain
policy already targets SLSA Build L3 for anything we ship (see
CLAUDE.md). Starting from a base
that already carries L3 provenance means the only provenance you have to generate
is your own layer.
docker login dhi.io # a free Docker account is enough
docker pull dhi.io/node:24-debian13
docker pull dhi.io/python:3.13
docker run --rm dhi.io/python:3.13 python -c "print('hello from DHI')"
Docker's own before/after on the Python image: 91% smaller (35 MB vs 412 MB) and 87% fewer packages (80 vs 610), clearing 1 high / 5 medium / 141 low findings.
The catch — and it is the whole point. Hardened images deliberately omit tooling you may expect. On a distroless runtime variant there is no shell, so
docker exec -it <ctr> shfails and aRUNstep that shells out breaks. That is the attack surface being gone, not a bug. Build in the-devvariant, ship the runtime one:
# syntax=docker/dockerfile:1
FROM dhi.io/node:24-debian13-dev AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM dhi.io/node:24-debian13 AS runner # runtime variant: no shell, no npm
WORKDIR /app
COPY --from=build /app/.next ./.next
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/public ./public
EXPOSE 3000
USER nonroot
CMD ["node_modules/.bin/next", "start"]
Note the CMD is an exec-form call to a real binary. CMD ["npm", "start"]
would need a shell in some images; on a distroless base, call the binary directly.
11. Scan images for CVEs — docker scout
Before an image ships, scan it. Docker Scout builds an SBOM (software bill of materials) from your image's layers and matches every package against a continuously updated vulnerability database — so you catch a known-vulnerable base image or transitive dependency before it's deployed, not after.
docker build -t myapp:dev .
docker scout quickview myapp:dev # one-line summary: how many CVEs, by severity
docker scout cves myapp:dev # the full list — package, CVE id, fixed-in version
docker scout recommendations myapp:dev # suggested base-image bumps that clear CVEs
quickview is the fast gate; cves is the detail when it flags something; recommendations often points at a newer -alpine/-slim base tag that clears the finding. Scout is built into Docker Desktop and the CLI — no separate install.
Beyond the three above, the subcommands you actually reach for:
| Command | Does |
|---|---|
docker scout quickview <img> |
One-line severity summary — the fast CI gate |
docker scout cves <img> |
Full finding list: package, CVE id, fixed-in version |
docker scout recommendations <img> |
Base-image bumps that clear findings |
docker scout sbom <img> |
Generate/print the image's SBOM |
docker scout compare <a> --to <b> |
Diff two images/tags (experimental) |
docker scout vex / attestation |
Manage VEX statements and in-toto attestations |
docker scout policy <img> |
Evaluate local Rego policies against an image (experimental) |
Scanning ≠ provenance. Scout tells you what's inside an image and whether it's vulnerable; SLSA provenance (see codeAmani notes) proves how and where the image was built. Shipped container images want both — they complement each other, neither replaces the other.
12. Docker's AI stack — Model Runner, MCP Toolkit, Offload
Three products that turn Docker from "the thing that runs my Postgres" into part of the AI toolchain. All three ship with Docker Desktop.
Docker Model Runner — local models, OpenAI-compatible
docker model pulls models from Docker Hub or Hugging Face as OCI artifacts and
serves them behind an OpenAI- and Ollama-compatible API. The mental model is
docker run, but the thing you run is a model:
docker model pull ai/qwen2.5-coder # from Docker Hub or Hugging Face
docker model run ai/qwen2.5-coder "Summarise this changelog"
docker model list # what's pulled locally
docker model ps # what's running
docker model status # is the runner up?
docker model configure --context-size 8192 ai/qwen2.5-coder
docker model df # disk used by models
docker model unload # free the VRAM
Because the endpoint is OpenAI-shaped, an existing openai client points at it by
changing only the base URL — no separate SDK.
Compose integration. A model can be a declared dependency of your stack, so
docker compose up starts the model alongside the app. The top-level models:
element declares it; the service references it and Compose injects the endpoint:
services:
app:
build: .
models:
my_model:
endpoint_var: MODEL_URL # Compose sets MODEL_URL in the container
models:
my_model:
model: ai/gemma3:4B-Q4_0
context_size: 10000
runtime_flags:
- "--a-flag"
With the short syntax (models: [my_model]) Compose injects a derived variable
instead; the long syntax above lets you name it.
Windows GPU requirements. On AMD64 you need an NVIDIA GPU with driver 576.57 or later. On ARM64 it runs via OpenCL on Qualcomm Adreno (6xx series and later), where some
llama.cppfeatures may not be fully supported. Without a supported GPU, expect CPU inference speeds.
Docker MCP Catalog & Toolkit — MCP servers as containers
The MCP Catalog is a curated set of 300+ verified MCP servers packaged as container images; the MCP Toolkit (a Docker Desktop tab) runs them and exposes them to MCP clients through a gateway. The payoff is that an MCP server's dependencies live in a container instead of on your machine, and you configure the set once rather than per client.
Three concepts: Catalogs (what is available), Profiles (named groups of
servers, e.g. web-dev), and Clients (Claude Code, Claude Desktop, VS Code,
Cursor...) that connect through the gateway.
# Connect a client to a profile's servers
docker mcp client connect vscode --profile my_profile
For Claude Desktop, Docker Desktop's MCP Toolkit -> Clients tab has a one-click Connect; restart the client afterwards. Note the enterprise MCP Gateway under Docker AI Governance is an invite-only feature — the Toolkit itself is not.
This is an alternative delivery mechanism for MCP servers, not a replacement for our own wiring. codeAmani's servers are configured directly in
MASTER_MCP_CONFIG.md; reach for the Toolkit when you want a third-party server without installing its runtime on the host.
Docker Offload — borrow a bigger machine
Docker Offload is a managed service that runs builds and containers in Docker's cloud using the same CLI you already use, then streams results back. It exists for the cases where local hardware is the blocker: a machine that cannot nest virtualization, a VDI environment, or a build that wants more cores than you own. Availability depends on your Docker subscription — check the Offload docs before designing around it.
13. Docker Sandboxes — run coding agents isolated
Docker Sandboxes run an AI coding agent inside a microVM: its own kernel, its own filesystem, its own network stack, and its own private Docker daemon. The agent can install packages, rewrite configs, and start containers, and your host is untouched. Claude Code is a natively supported agent.
The CLI is sbx — note it is not docker sandbox, and it needs neither
Docker Desktop nor Docker Engine installed.
# Windows 11 — install and authenticate
winget install -h Docker.sbx
sbx login
sbx run claude # launch Claude Code inside a fresh sandbox
Windows gotcha — this one does not ride on WSL 2. Unlike everything else in this guide,
sbxneeds Windows 11 with the Windows Hypervisor Platform feature enabled (it runs its own microVM). Ifsbxcannot start a sandbox on a machine where Docker Desktop works fine, this is why — enable the feature and reboot. macOS needs Sonoma 14+ on Apple silicon; Linux needs Ubuntu 24.04+ with KVM.
The sbx CLI is free to use, including for commercial work. Network access is
governed by configurable allow/deny lists, which is the point: an agent running
unsupervised should not be able to reach arbitrary hosts.
When it earns its keep: letting an agent run a risky migration, a dependency
upgrade, or an untrusted build without staking your host on it. Compare with the
WSL Ubuntu sandbox, which gives a disposable
distro — cheaper and already on your machine, but sharing the host kernel and your
Docker daemon. sbx is the stronger boundary; the WSL sandbox is the lighter one.
14. 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.
Three levels of isolation, cheapest first — pick by how much you trust the task:
| Level | What it is | Blast radius |
|---|---|---|
docker run --rm a throwaway container |
One process, shared kernel + your daemon | The container |
| WSL Ubuntu sandbox | A disposable distro you can export/import | That distro |
sbx run claude (§13) |
microVM: own kernel, own Docker daemon, network policy | The sandbox |
Unsupervised or untrusted work belongs at the bottom row. Routine "build this and run the tests" is fine at the top.
15. 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 |
the attribute version is obsolete on compose up |
Delete the top-level version: key from compose.yaml — it's informational only now |
docker-compose: command not found |
Use docker compose (space) — the v1 hyphenated binary is EOL/removed |
429 Too Many Requests pulling a base image |
Docker Hub anonymous limit (100/6h per IP, shared across a NAT/CI runner) — docker login in CI (§10) |
docker exec -it <ctr> sh fails: exec: "sh": not found |
A distroless/hardened image has no shell by design (§10) — debug via the -dev variant or docker logs |
docker model run is very slow on Windows |
No supported GPU — AMD64 needs NVIDIA driver 576.57+; otherwise it is CPU inference (§12) |
sbx won't start a sandbox (Docker Desktop is fine) |
sbx uses its own microVM, not WSL 2 — enable Windows Hypervisor Platform and reboot (§13) |
Migration races the database on compose up |
depends_on: service_started only waits for existence — use service_healthy + pre_start (§6) |
Secret visible in docker history |
It was a --build-arg/ENV — use --mount=type=secret instead (§5) |
| 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 |
16. 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. - Prefer a hardened base for anything we ship.
dhi.ioimages (§10) arrive with near-zero CVEs, a signed SBOM, VEX, and SLSA Build L3 provenance — the exact target our supply-chain policy sets. The community catalog is free, so the only real cost is adapting to a missing shell: build in the-devvariant, ship the runtime one. For images we merely deploy (Cloud Run/Render), a pinned-alpine/-slimbase plus a Scout gate is still fine. - Local models are a cost lever, not a quality one. Model Runner (§12) serves an
OpenAI-compatible endpoint, so it drops into our existing clients by base-URL swap.
It suits offline/dev work and cheap bulk passes; it does not replace the
AI routing policy — Claude stays primary for reasoning and code
generation. A Compose
models:block keeps a dev stack self-contained. - Scan, then prove provenance, on anything we ship.
docker scout cves(§10) gates on what's inside the image before it deploys. For an image we ship as a downloadable artifact, that pairs with SLSA Build L3 container provenance — the container generator +slsa-verifier verify-imageper the SLSA policy inCLAUDE.md. Scanning proves the contents are clean; provenance proves how/where it was built — ship both. (Images we only deploy to Cloud Run/Render are deploys, not artifacts — scan them, pin the build, skip the L3 target.) - 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). For running just a database in a container (the most common case), see the local-database guide; for a throwaway Linux workspace to run untrusted builds, see the sandbox guide. Both sit on the same WSL 2 backend this guide does. - 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.
Official docs:
- https://docs.docker.com/desktop/setup/install/windows-install/
- https://docs.docker.com/desktop/features/wsl/
- https://docs.docker.com/get-started/
- https://docs.docker.com/reference/cli/docker/
- https://docs.docker.com/compose/
- https://docs.docker.com/reference/dockerfile/
- https://docs.docker.com/build/building/best-practices/
- https://docs.docker.com/build/buildkit/
- https://docs.docker.com/scout/
- https://docs.docker.com/dhi/
- https://docs.docker.com/ai/model-runner/
- https://docs.docker.com/ai/mcp-catalog-and-toolkit/
- https://docs.docker.com/ai/sandboxes/