← Back to dashboard

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 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; `pre_start` init containers run migrations before the app boots; 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`) or via `RUN --mount=type=secret` at build time — never `ARG`/`ENV`, which `docker history` prints straight back. In 2026 Docker also ships hardened base images on `dhi.io` carrying SLSA Build L3 provenance, a local OpenAI-compatible model runtime, and `sbx` microVM sandboxes that run Claude Code with no access to your host.

Seven Docker building blocks

An image is the frozen template; a container is it running. The rest is how you build, compose, persist — and, in 2026, how you isolate an agent.

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, base-image choice (Hub rate limits vs Docker Hardened Images), scanning with docker 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 in docs.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

  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. Base images: Hub limits & Docker Hardened Images
  11. Scan images for CVEs — docker scout
  12. Docker's AI stack — Model Runner, MCP Toolkit, Offload
  13. Docker Sandboxes — run coding agents isolated
  14. Claude Code + Docker
  15. Troubleshooting
  16. 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) — 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:

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.

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:

PowerShell
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.

✅ 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
# 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:1 line opts into its latest frontend, enabling parallel stages, cache mounts (RUN --mount=type=cache), and build secrets (--mount=type=secret, §12). docker buildx is 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):

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.

BuildKit flags worth knowing

The # syntax=docker/dockerfile:1 line pins the latest stable frontend, so these are available without extra config:

FlagSinceWhat it buys you
RUN --mount=type=cache,target=...v1.2Persist a package-manager cache across builds — npm/pip/apt stop re-downloading
RUN --mount=type=secret,id=...v1.2Read a secret during build without baking it into a layer
RUN --mount=type=bind,from=...v1.2Read files from another stage/context without a COPY layer
RUN --mount=type=sshv1.2Use the host SSH agent for private-repo git clone
ADD --checksum=sha256:...v1.6Verify a remote download — pin it or don't trust it
COPY --exclude=...v1.19Skip paths inside a COPY without touching .dockerignore
COPY --parentsv1.20Preserve the source directory structure when copying globs
Dockerfile
# 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
Bash
# 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 .

ARG and ENV are not secret. Both are recorded in the image's build history — docker history prints them back. A token passed as --build-arg is a published token. Use --mount=type=secret for 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 compose always validates against the latest schema). Start the file at services:. docker init and the examples here already omit it; don't add it back.

YAML
# 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:
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.

docker compose (space), not docker-compose (hyphen). Compose is now a Docker CLI plugin (the docker compose subcommand, currently v5.5.0, bundled with Docker Desktop). The old standalone Python docker-compose v1 reached end of life in 2024 and was removed in 2025 — if a script still calls the hyphenated form it's running unmaintained software. Convert docker-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:

YAML
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_started only waits for the container to exist. A Postgres container exists long before it accepts connections. Use condition: service_healthy with a real healthcheck: (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:

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:17   # 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

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 pullingLimit (per 6 hours)
Unauthenticated100 — per IPv4 address or IPv6 /64 subnet
Authenticated personal account (free)200
Pro / Team / BusinessUnlimited

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:

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

Bash
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.

Bash
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> sh fails and a RUN step that shells out breaks. That is the attack surface being gone, not a bug. Build in the -dev variant, ship the runtime one:

Dockerfile
# 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.

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

CommandDoes
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 / attestationManage 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:

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

YAML
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.cpp features 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.

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

PowerShell
# Windows 11 — install and authenticate
winget install -h Docker.sbx
sbx login
Bash
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, sbx needs Windows 11 with the Windows Hypervisor Platform feature enabled (it runs its own microVM). If sbx cannot 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.

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.

Three levels of isolation, cheapest first — pick by how much you trust the task:

LevelWhat it isBlast radius
docker run --rm a throwaway containerOne process, shared kernel + your daemonThe container
WSL Ubuntu sandboxA disposable distro you can export/importThat distro
sbx run claude (§13)microVM: own kernel, own Docker daemon, network policyThe sandbox

Unsupervised or untrusted work belongs at the bottom row. Routine "build this and run the tests" is fine at the top.


15. 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
the attribute version is obsolete on compose upDelete the top-level version: key from compose.yaml — it's informational only now
docker-compose: command not foundUse docker compose (space) — the v1 hyphenated binary is EOL/removed
429 Too Many Requests pulling a base imageDocker 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 foundA distroless/hardened image has no shell by design (§10) — debug via the -dev variant or docker logs
docker model run is very slow on WindowsNo 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 updepends_on: service_started only waits for existence — use service_healthy + pre_start (§6)
Secret visible in docker historyIt was a --build-arg/ENV — use --mount=type=secret instead (§5)
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

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, 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.
  • Prefer a hardened base for anything we ship. dhi.io images (§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 -dev variant, ship the runtime one. For images we merely deploy (Cloud Run/Render), a pinned -alpine/-slim base 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-image per the SLSA policy in CLAUDE.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/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). 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/-slim bases to keep images and pulls small on metered connections.