Skip to content

Commit efc464f

Browse files
authored
fix(devcontainers): restore XDG dirs so mise works under nested volume mounts (#150)
24afdcf replaced mkdir -p /home/node/.local/share/fish with /home/node/.local, dropping the .local/share level that mkdir -p had created as a side effect. Consuming projects still mount a *-fish-data volume at ~/.local/share/fish, and Docker invents a missing mount parent as root:root -- so .local/share became root-owned and mise could no longer create ~/.local/share/mise. Every consuming devcontainer fails 'mise install' on its next rebuild. Creates .local/share, .local/state, .config and .cache node-owned (the four dirs mise writes to) in both affected images. Reproduced against the published image and verified with a volume mounted at the nested path -- a plain docker run does not surface it. CI now asserts .local/share is node-owned, since the failure is silent until a consumer rebuilds. Also tightens comments across the Dockerfiles and renovate.json5: claude-code 45%->38% comment lines, renovate.json5 42%->30%. Package-manifest lists are left alone -- one short line per package is the useful form.
1 parent 5ad4c53 commit efc464f

6 files changed

Lines changed: 85 additions & 152 deletions

File tree

‎.devcontainer/Dockerfile‎

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,8 @@
1919

2020
# ── rtk (token-optimized CLI proxy) ──────────────────────────────────────
2121
# ── oh-my-zsh + powerlevel10k ─────────────────────────────────
22-
# Pinned to commit SHAs and fetched as tarballs. Neither project tags usefully --
23-
# oh-my-zsh has no tags at all, and powerlevel10k's last release predates its
24-
# current master by years -- so a SHA is the only real pin. Bump deliberately:
25-
# auto-adopting upstream shell-framework changes is what repeatedly broke this
26-
# image, and a prompt theme does not need a 3-day release cadence.
22+
# Pinned by commit SHA: neither project tags usefully, and auto-adopting
23+
# shell-framework changes has broken this image before. Bump deliberately.
2724
FROM alpine:3.21 AS ohmyzsh-download
2825
RUN apk add --no-cache curl tar
2926
ARG OH_MY_ZSH_REF=0ee67f042872d1dfab74270c31867771ca35aef4
@@ -116,16 +113,18 @@ RUN mkdir -p /usr/local/share/npm-global/lib \
116113

117114
ENV DEVCONTAINER=true
118115

119-
# Create workspace and config directories with proper ownership
120-
RUN mkdir -p /workspace /home/node/.claude /home/node/.local /commandhistory \
121-
&& chown -R node:node /workspace /home/node/.claude /home/node/.local /commandhistory
116+
# Workspace and XDG dirs, node-owned: a volume mounted at a nested path under one
117+
# of these makes Docker invent the missing parent as root, locking node out.
118+
RUN mkdir -p /workspace /home/node/.claude \
119+
/home/node/.local/share /home/node/.local/state /home/node/.config /home/node/.cache \
120+
/commandhistory \
121+
&& chown -R node:node /workspace /home/node/.claude /home/node/.local \
122+
/home/node/.config /home/node/.cache /commandhistory
122123

123124
WORKDIR /workspace
124125

125-
# gh (GitHub CLI) — installed from the upstream .deb, not apt: Debian trixie
126-
# freezes gh at 2.46.0 (Apr 2024). Version kept current by Renovate.
127-
# Must stay AFTER the apt block: the .deb depends on git, and dpkg -i does not
128-
# resolve dependencies — it fails loudly ("dependency problems") if git is absent.
126+
# gh from the upstream .deb: trixie freezes it at 2.46.0 (Apr 2024). Must follow
127+
# the apt block — the .deb depends on git and dpkg -i won't resolve it.
129128
RUN ARCH=$(dpkg --print-architecture) \
130129
&& curl -fsSL -o "gh_${GH_VERSION}_linux_${ARCH}.deb" \
131130
"https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_${ARCH}.deb" \
@@ -138,28 +137,20 @@ USER node
138137
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
139138
ENV PATH=$PATH:/usr/local/share/npm-global/bin
140139
ENV SHELL=/usr/bin/zsh
141-
# Only LANG, and only C.UTF-8 — what the official images do (ruby sets exactly
142-
# this; node/debian/python set nothing). LC_ALL would override every LC_* category
143-
# and silently defeat a consuming project's containerEnv. TERM is deliberately
144-
# unset: no official image sets it, an image ENV beats the tty value `docker exec -t`
145-
# supplies, and tput/clear failing without a terminal is correct behaviour.
140+
# LANG only, as the official images do. LC_ALL would outrank a consumer's
141+
# containerEnv; TERM stays unset so the terminal's own value wins.
146142
ENV LANG=C.UTF-8
147143
ENV EDITOR="code --wait"
148144
ENV VISUAL="code --wait"
149145

150146
# ── zsh + oh-my-zsh + powerlevel10k ──────────────────────────────────────────
151147
# Written as node: $HOME during RUN follows USER, so this must not run as root.
152148
USER node
153-
# oh-my-zsh tree is COPYed from the parallel download stage above, so the final
154-
# image needs no git or curl for it and nothing is fetched at build time here.
155149
COPY --from=ohmyzsh-download --chown=node:node /omz /home/node/.oh-my-zsh
156150

157-
# .zshrc is written here rather than by an installer, so nothing upstream can
158-
# inject env overrides (locale/TERM) that then have to be patched back out.
159-
# The wizard flag matters: without a POWERLEVEL9K_* config powerlevel10k starts
160-
# its interactive configurator on first shell, which blocks a container.
161-
# $HOME and $ZSH are single-quoted on purpose: they must reach .zshrc
162-
# unexpanded so zsh resolves them at runtime, not at build time.
151+
# .zshrc written here, not by an installer, so nothing upstream injects env
152+
# overrides. The wizard flag stops powerlevel10k prompting on first shell.
153+
# Single-quoted so $HOME/$ZSH reach .zshrc unexpanded.
163154
# hadolint ignore=SC2016
164155
RUN printf '%s\n' \
165156
'export ZSH="$HOME/.oh-my-zsh"' \

‎.github/renovate.json5‎

Lines changed: 16 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@
66
// github-actions managers open PRs for base images or action pins (out of scope).
77
enabledManagers: ['custom.regex'],
88

9-
// Pin + auto-update the four dev tools these images used to pull from
10-
// "latest" at build time. Replaces the old daily rebuild cron: a Renovate
11-
// bump PR (auto-merged on green CI) triggers the existing push-based image
12-
// build. No upstream release -> no PR -> no rebuild.
9+
// Pins tools these images used to pull at build time. Replaces the daily cron:
10+
// a bump PR triggers the push-based build. No release -> no PR -> no rebuild.
1311
customManagers: [
1412
{
1513
customType: 'regex',
@@ -22,25 +20,17 @@
2220

2321
packageRules: [
2422
{
25-
// Most GitHub tags carry a leading "v" ("v0.48.0", "v29.8.1"); strip it so the
26-
// datasource version matches the bare ARG value ("0.48.0"). A tag with no "v"
27-
// passes through unchanged, so this is safe to apply datasource-wide.
23+
// Strip the leading "v" so tags match the bare ARG value. Tags without one
24+
// pass through unchanged, so this is safe datasource-wide.
2825
matchDatasources: ['github-releases', 'github-tags'],
2926
extractVersion: '^v?(?<version>.+)$',
3027
},
3128
{
32-
// Group the tracked tools into one PR and auto-merge once CI passes.
33-
//
34-
// Relies on Renovate's default platformAutomerge:true — GitHub's native
35-
// auto-merge merges on green with no second Renovate run. The previous
36-
// explicit platformAutomerge:false is what broke this: only a Renovate
37-
// run could merge, and every run found a newer claude-code, force-pushed
38-
// the branch and ended before CI finished (#121 sat green for 3.5 weeks).
39-
//
40-
// Requires: repo setting "Allow auto-merge", and a ruleset on master
41-
// requiring the "CI complete" check (.github/workflows/ci.yml). Without
42-
// that required check nothing blocks the PR, GitHub never offers native
43-
// auto-merge, and Renovate silently falls back to the broken path.
29+
// One grouped PR, auto-merged on green via Renovate's default
30+
// platformAutomerge. Setting it false broke this before: only a Renovate run
31+
// could merge, and each run force-pushed a newer claude-code before CI
32+
// finished (#121 sat green 3.5 weeks). Requires "Allow auto-merge" plus a
33+
// master ruleset requiring the "CI complete" check, or it silently regresses.
4434
matchPackageNames: [
4535
'rtk-ai/rtk',
4636
'umputun/ralphex',
@@ -57,10 +47,8 @@
5747
groupName: 'devcontainer tools',
5848
automerge: true,
5949

60-
// These bumps merge unreviewed and publish straight to ghcr.io, so let a
61-
// release soak before adopting it. internalChecksFilter defaults to
62-
// 'strict', so a too-young version is simply not offered yet — the group
63-
// PR carries whichever tools are currently eligible.
50+
// These merge unreviewed and publish straight to ghcr.io, so let releases
51+
// soak. Too-young versions are simply not offered; the PR carries the rest.
6452
minimumReleaseAge: '3 days',
6553
},
6654
{
@@ -70,21 +58,17 @@
7058
minimumReleaseAge: null,
7159
},
7260
{
73-
// ralphex-fe bakes its toolchain into the image — it has no mise, and
74-
// its published tag is literally bun<BUN>-hugo<HUGO>. So these are
75-
// image-level versions, not per-project ones, and there is no other
76-
// place to set them. Bun uses the npm datasource because oven-sh/bun
77-
// tags releases as "bun-v1.4.2", which the github-releases extractVersion
78-
// above does not strip.
61+
// ralphex-fe has no mise and its tag is literally bun<BUN>-hugo<HUGO>, so
62+
// these are image-level versions. Bun uses npm: its tags are "bun-v1.4.2",
63+
// which the extractVersion above does not strip.
7964
matchPackageNames: ['bun', 'gohugoio/hugo'],
8065
groupName: 'ralphex-fe toolchain',
8166
automerge: true,
8267
minimumReleaseAge: '3 days',
8368
},
8469
{
85-
// hugo-bun-node installs Node from the unofficial musl builds — its base
86-
// image (oven/bun:*-alpine) ships no node at all. The image is Node 24 LTS
87-
// by design, so major bumps stay a deliberate call, not a Renovate PR.
70+
// hugo-bun-node's alpine base ships no node, so it installs the musl build.
71+
// Node 24 LTS by design — majors stay a deliberate call.
8872
matchDatasources: ['node-version'],
8973
allowedVersions: '^24',
9074
},

‎.github/workflows/build-claude-code.yml‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,19 +75,19 @@ jobs:
7575
matrix:
7676
include:
7777
- image-suffix: claude-code
78-
verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k"
78+
verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node"
7979
runner: ubuntu-24.04
8080
arch: amd64
8181
- image-suffix: claude-code
82-
verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k"
82+
verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node"
8383
runner: ubuntu-24.04-arm
8484
arch: arm64
8585
- image-suffix: claude-code-sandbox
86-
verify-command: "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k"
86+
verify-command: "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node"
8787
runner: ubuntu-24.04
8888
arch: amd64
8989
- image-suffix: claude-code-sandbox
90-
verify-command: "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k"
90+
verify-command: "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node"
9191
runner: ubuntu-24.04-arm
9292
arch: arm64
9393
runs-on: ${{ matrix.runner }}

‎.github/workflows/ci.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,12 @@ jobs:
117117
add_image "claude-code" \
118118
"claude-code/.devcontainer" \
119119
"claude-code/.devcontainer/Dockerfile" \
120-
"bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k" \
120+
"bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node" \
121121
"default"
122122
add_image "claude-code-sandbox" \
123123
"claude-code/.devcontainer" \
124124
"claude-code/.devcontainer/Dockerfile" \
125-
"claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k" \
125+
"claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node" \
126126
"sandbox"
127127
fi
128128

‎claude-bun/.devcontainer/Dockerfile‎

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
ARG BUN_VERSION=1.3.5
22

33
# ── oh-my-zsh + powerlevel10k ─────────────────────────────────
4-
# Pinned to commit SHAs and fetched as tarballs. Neither project tags usefully --
5-
# oh-my-zsh has no tags at all, and powerlevel10k's last release predates its
6-
# current master by years -- so a SHA is the only real pin. Bump deliberately:
7-
# auto-adopting upstream shell-framework changes is what repeatedly broke this
8-
# image, and a prompt theme does not need a 3-day release cadence.
4+
# Pinned by commit SHA: neither project tags usefully, and auto-adopting
5+
# shell-framework changes has broken this image before. Bump deliberately.
96
FROM alpine:3.21 AS ohmyzsh-download
107
RUN apk add --no-cache curl tar
118
ARG OH_MY_ZSH_REF=0ee67f042872d1dfab74270c31867771ca35aef4
@@ -66,10 +63,8 @@ RUN mkdir -p /workspace /home/bun/.claude /home/bun/.bun && \
6663

6764
WORKDIR /workspace
6865

69-
# gh (GitHub CLI) — installed from the upstream .deb, not apt: Debian trixie
70-
# freezes gh at 2.46.0 (Apr 2024). Version kept current by Renovate.
71-
# Must stay AFTER the apt block: the .deb depends on git, and dpkg -i does not
72-
# resolve dependencies — it fails loudly ("dependency problems") if git is absent.
66+
# gh from the upstream .deb: trixie freezes it at 2.46.0 (Apr 2024). Must follow
67+
# the apt block — the .deb depends on git and dpkg -i won't resolve it.
7368
RUN ARCH=$(dpkg --print-architecture) && \
7469
curl -fsSL -o "gh_${GH_VERSION}_linux_${ARCH}.deb" \
7570
"https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_${ARCH}.deb" && \
@@ -87,27 +82,19 @@ ENV PATH="$BUN_INSTALL/bin:$PATH"
8782
# Set the default shell to zsh rather than sh (/usr/bin/zsh, matching the other
8883
# images; /bin/zsh is the same binary via usrmerge)
8984
ENV SHELL=/usr/bin/zsh
90-
# Only LANG, and only C.UTF-8 — what the official images do (ruby sets exactly
91-
# this; node/debian/python set nothing). LC_ALL would override every LC_* category
92-
# and silently defeat a consuming project's containerEnv. TERM is deliberately
93-
# unset: no official image sets it, an image ENV beats the tty value `docker exec -t`
94-
# supplies, and tput/clear failing without a terminal is correct behaviour.
85+
# LANG only, as the official images do. LC_ALL would outrank a consumer's
86+
# containerEnv; TERM stays unset so the terminal's own value wins.
9587
ENV LANG=C.UTF-8
9688

9789
# zsh + oh-my-zsh + powerlevel10k.
9890
#
9991
# Written as bun: $HOME during RUN follows USER, so this must not run as root.
10092
USER bun
101-
# oh-my-zsh tree is COPYed from the parallel download stage above, so the final
102-
# image needs no git or curl for it and nothing is fetched at build time here.
10393
COPY --from=ohmyzsh-download --chown=bun:bun /omz /home/bun/.oh-my-zsh
10494

105-
# .zshrc is written here rather than by an installer, so nothing upstream can
106-
# inject env overrides (locale/TERM) that then have to be patched back out.
107-
# The wizard flag matters: without a POWERLEVEL9K_* config powerlevel10k starts
108-
# its interactive configurator on first shell, which blocks a container.
109-
# $HOME and $ZSH are single-quoted on purpose: they must reach .zshrc
110-
# unexpanded so zsh resolves them at runtime, not at build time.
95+
# .zshrc written here, not by an installer, so nothing upstream injects env
96+
# overrides. The wizard flag stops powerlevel10k prompting on first shell.
97+
# Single-quoted so $HOME/$ZSH reach .zshrc unexpanded.
11198
# hadolint ignore=SC2016
11299
RUN printf '%s\n' \
113100
'export ZSH="$HOME/.oh-my-zsh"' \

0 commit comments

Comments
 (0)