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/c crosses the OS boundary on every file op and crawls; the same build from ~/code runs at native speed. Enable WSL integration once, run the docker CLI 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 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

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.

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

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

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

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 .

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.

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

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:

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:


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

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:

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

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

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

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

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

Official docs: