Skip to content

Commit 22d2e1b

Browse files
gatezhclaude
andauthored
fix(claude-code): drop pull_policy:always, pull image via initializeCommand (#109) (#112)
pull_policy: always in the Compose templates is incompatible with updateRemoteUserUID:true on Linux/WSL2. The Dev Containers CLI builds a local-only vsc-...-uid image to remap the node user, and the generated Compose override points image: at that local tag while inheriting pull_policy: always, so 'docker compose up' tries to pull an image that exists only on disk and fails with 'pull access denied'. Masked on macOS/Windows Docker Desktop, where no -uid image is built. Move image refresh to a host-side initializeCommand in devcontainer.json, which runs before the build (Initialization step) so the -uid image layers on a freshly-pulled base, and drop pull_policy from both Compose files. The '|| exit 0' guard keeps offline/outage opens working and is portable across the host shells the CLI uses for string commands (POSIX sh + Windows cmd.exe). Updates default and sandbox variants and the README. Closes #109 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3819277 commit 22d2e1b

5 files changed

Lines changed: 28 additions & 9 deletions

File tree

‎claude-code/.devcontainer/claude-sandbox/devcontainer.json‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
"dockerComposeFile": "docker-compose.yml",
55
"service": "devcontainer",
66
"workspaceFolder": "/workspace",
7+
// Pull the prebuilt image on the HOST before the build, so "Rebuild Without
8+
// Cache" always layers on the freshest base. Replaces `pull_policy: always` in
9+
// docker-compose.yml, which breaks on Linux/WSL2 when `updateRemoteUserUID`
10+
// builds a local-only image (issue #109). `|| exit 0` keeps offline/outage opens
11+
// working (valid in POSIX sh and Windows cmd.exe). Keep in sync with the compose image.
12+
"initializeCommand": "docker pull ghcr.io/gatezh/devcontainers/claude-code-sandbox:latest || exit 0",
713
// Capabilities required for iptables firewall setup
814
"capAdd": ["NET_ADMIN", "NET_RAW"],
915
"init": true,

‎claude-code/.devcontainer/claude-sandbox/docker-compose.yml‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
name: myproject-sandbox
33
services:
44
devcontainer:
5+
# Image freshness is handled by the host-side `initializeCommand` pull in
6+
# devcontainer.json (keep the image reference in sync). Do NOT add
7+
# `pull_policy: always` here — on Linux/WSL2 `updateRemoteUserUID` builds a
8+
# local-only image and Compose would then try to pull it, breaking the
9+
# container launch with "pull access denied" (see issue #109).
510
image: ghcr.io/gatezh/devcontainers/claude-code-sandbox:latest
6-
pull_policy: always
711
volumes:
812
- ../..:/workspace:cached

‎claude-code/.devcontainer/devcontainer.json‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
"dockerComposeFile": "docker-compose.yml",
55
"service": "devcontainer",
66
"workspaceFolder": "/workspace",
7+
// Pull the prebuilt image on the HOST before the build, so "Rebuild Without
8+
// Cache" always layers on the freshest base. Replaces `pull_policy: always` in
9+
// docker-compose.yml, which breaks on Linux/WSL2 when `updateRemoteUserUID`
10+
// builds a local-only image (issue #109). `|| exit 0` keeps offline/outage opens
11+
// working (valid in POSIX sh and Windows cmd.exe). Keep in sync with the compose image.
12+
"initializeCommand": "docker pull ghcr.io/gatezh/devcontainers/claude-code:latest || exit 0",
713
"init": true,
814
"updateRemoteUserUID": true,
915
"remoteUser": "node",

‎claude-code/.devcontainer/docker-compose.yml‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
name: myproject
33
services:
44
devcontainer:
5+
# Image freshness is handled by the host-side `initializeCommand` pull in
6+
# devcontainer.json (keep the image reference in sync). Do NOT add
7+
# `pull_policy: always` here — on Linux/WSL2 `updateRemoteUserUID` builds a
8+
# local-only image and Compose would then try to pull it, breaking the
9+
# container launch with "pull access denied" (see issue #109).
510
image: ghcr.io/gatezh/devcontainers/claude-code:latest
6-
pull_policy: always
711
volumes:
812
- ..:/workspace:cached

‎claude-code/README.md‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,13 @@ The image rebuilds daily at 5am MT (11:00 UTC) using native runners for both amd
4848

4949
### Default variant
5050

51-
Copy the example files into your project's `.devcontainer/` directory and customize as needed. Docker Compose with `pull_policy: always` ensures "Rebuild Without Cache" always pulls the latest image. All other config stays in `devcontainer.json` using cross-orchestrator properties (`mounts`, `containerEnv`, `capAdd`, `init`) so you keep devcontainer variable substitution (`${localWorkspaceFolderBasename}`, `${localEnv:...}`).
51+
Copy the example files into your project's `.devcontainer/` directory and customize as needed. A host-side `initializeCommand` in `devcontainer.json` pulls the prebuilt image before the container is built, so "Rebuild Without Cache" always layers on the latest image. All other config stays in `devcontainer.json` using cross-orchestrator properties (`mounts`, `containerEnv`, `capAdd`, `init`) so you keep devcontainer variable substitution (`${localWorkspaceFolderBasename}`, `${localEnv:...}`).
5252

5353
**After copying:** replace `myproject` with your project name in `docker-compose.yml` (the `name:` field) and `devcontainer.json` (volume mount prefixes). This must match across both variants if using the sandbox.
5454

5555
Copy these to your project's `.devcontainer/`:
5656

57-
- [`.devcontainer/docker-compose.yml`](.devcontainer/docker-compose.yml) — image reference with `pull_policy: always`
57+
- [`.devcontainer/docker-compose.yml`](.devcontainer/docker-compose.yml) — image reference (kept fresh by the `initializeCommand` pull in `devcontainer.json`)
5858
- [`.devcontainer/devcontainer.json`](.devcontainer/devcontainer.json) — full config with VS Code extensions, fish shell, OXC formatter, node_modules volume isolation, and lifecycle commands
5959

6060
**Key settings included:** fish + bash terminal profiles, OXC formatter (with comments for switching to Biome/Prettier), node_modules/Claude config/fish history volume mounts, and `updateContentCommand` for mise/bun setup.
@@ -114,7 +114,7 @@ Mark as executable: `chmod +x init-plugins.sh`
114114

115115
To remove a plugin in your project, delete its entry from the local `init-plugins.sh` — the script is a template, not image-baked, so each consumer controls its own list.
116116

117-
> **Why `init-plugins.sh` stays per-project but `patch-playwright-mcp` doesn't:** `init-plugins.sh` carries project-specific configuration (marketplace list, plugin list) — it's *meant* to be edited per project. The patch script has zero project-specific config and is identical across every consumer, so it's baked into the image and flows through the same daily-rebuild + `pull_policy: always` channel as the rest of the image. That boundary is the rule: project-specific config stays per-project; universal logic moves into the image.
117+
> **Why `init-plugins.sh` stays per-project but `patch-playwright-mcp` doesn't:** `init-plugins.sh` carries project-specific configuration (marketplace list, plugin list) — it's *meant* to be edited per project. The patch script has zero project-specific config and is identical across every consumer, so it's baked into the image and flows through the same daily-rebuild + `initializeCommand` image-pull channel as the rest of the image. That boundary is the rule: project-specific config stays per-project; universal logic moves into the image.
118118
119119
### Sandbox-only: `.devcontainer/claude-sandbox/init-firewall.sh`
120120

@@ -224,11 +224,11 @@ The template includes extensions for Claude Code, Bun, OXC, Tailwind, YAML, Dock
224224
```
225225
.devcontainer/
226226
├── devcontainer.json ← default devcontainer
227-
├── docker-compose.yml ← default compose (image + pull_policy)
227+
├── docker-compose.yml ← default compose (image reference)
228228
├── init-plugins.sh ← Claude Code plugin setup (optional)
229229
└── claude-sandbox/
230230
├── devcontainer.json ← sandbox devcontainer
231-
├── docker-compose.yml ← sandbox compose (image + pull_policy)
231+
├── docker-compose.yml ← sandbox compose (image reference)
232232
├── init-firewall.sh ← firewall script (customize domain allowlist)
233233
├── .env.example ← template for auth token (checked in)
234234
└── .env.local ← actual auth token (gitignored)
@@ -431,10 +431,9 @@ services:
431431
# image: ghcr.io/gatezh/devcontainers/claude-code:latest
432432
# With the local build:
433433
image: claude-code:local
434-
# pull_policy no longer needed for local images
435434
```
436435
437-
Everything else in `devcontainer.json` (mounts, containerEnv, lifecycle commands, etc.) stays the same — the local image is identical to the pre-built one.
436+
Everything else in `devcontainer.json` (mounts, containerEnv, lifecycle commands, etc.) stays the same — the local image is identical to the pre-built one. The `initializeCommand` still points at the GHCR image, but `|| exit 0` means a failed pull won't block the open; you can remove it to skip the now-unnecessary pull.
438437
439438
### Extending the image for project-specific needs
440439

0 commit comments

Comments
 (0)