1111# docker build --target sandbox -t claude-code:sandbox .
1212# ═══════════════════════════════════════════════════════════════════════════════
1313
14+ # ═════════════════════════════════════════════════════════════════════════════
15+ # Parallel download stages — BuildKit runs these concurrently
16+ # ═════════════════════════════════════════════════════════════════════════════
17+
18+ # ── rtk (token-optimized CLI proxy) ──────────────────────────────────────
19+ FROM alpine:3.21 AS rtk-download
20+ RUN apk add --no-cache curl jq
21+ RUN set -eux; \
22+ ARCH="$(uname -m)" ; \
23+ case "$ARCH" in \
24+ x86_64) RTK_TARGET="x86_64-unknown-linux-musl" ;; \
25+ aarch64) RTK_TARGET="aarch64-unknown-linux-gnu" ;; \
26+ esac; \
27+ RTK_VERSION=$(curl -fsSL https://api.github.com/repos/rtk-ai/rtk/releases/latest \
28+ | jq -r '.tag_name' | sed 's/^v//' ); \
29+ curl -fsSL -o /tmp/rtk.tar.gz \
30+ "https://github.com/rtk-ai/rtk/releases/download/v${RTK_VERSION}/rtk-${RTK_TARGET}.tar.gz" ; \
31+ tar -xzf /tmp/rtk.tar.gz -C /usr/local/bin rtk
32+
33+ # ── ralphex (autonomous plan execution) ──────────────────────────────────
34+ FROM alpine:3.21 AS ralphex-download
35+ RUN apk add --no-cache curl jq
36+ RUN set -eux; \
37+ ARCH="$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')" ; \
38+ RALPHEX_VERSION=$(curl -fsSL https://api.github.com/repos/umputun/ralphex/releases/latest \
39+ | jq -r '.tag_name' | sed 's/^v//' ); \
40+ curl -fsSL -o /tmp/ralphex.tar.gz \
41+ "https://github.com/umputun/ralphex/releases/download/v${RALPHEX_VERSION}/ralphex_${RALPHEX_VERSION}_linux_${ARCH}.tar.gz" ; \
42+ tar -xzf /tmp/ralphex.tar.gz -C /usr/local/bin ralphex
43+
1444# ─── BASE ─────────────────────────────────────────────────────────────────────
1545FROM node:24-trixie-slim AS base
1646
@@ -27,7 +57,9 @@ ARG PLAYWRIGHT_VERSION=1.58.2
2757# - jq: JSON processing (firewall script, onboarding patch)
2858# - less: pager for git delta output
2959# - sudo: privilege escalation for firewall setup
30- RUN apt-get update && apt-get install -y --no-install-recommends \
60+ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
61+ --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
62+ apt-get update && apt-get install -y --no-install-recommends \
3163 ca-certificates \
3264 curl \
3365 fish \
@@ -36,8 +68,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
3668 git \
3769 jq \
3870 less \
39- sudo \
40- && apt-get clean && rm -rf /var/lib/apt/lists/*
71+ sudo
4172
4273# npm global directory with proper permissions for node user
4374# Pre-create /lib to prevent "ENOENT" errors during npx commands
@@ -81,9 +112,11 @@ ENV VISUAL="code --wait"
81112
82113# ── Starship + Mise (install as root, configure as node) ───────────────────────
83114USER root
115+ SHELL ["/bin/bash" , "-o" , "pipefail" , "-c" ]
84116RUN curl -sS https://starship.rs/install.sh | sh -s -- --yes
85117RUN curl https://mise.run | sh \
86118 && cp /root/.local/bin/mise /usr/local/bin/mise
119+ SHELL ["/bin/sh" , "-c" ]
87120
88121# Configure starship and fish shell.
89122# Mise is installed as a tool manager — projects run `mise install` at container
@@ -97,27 +130,12 @@ RUN mkdir -p /home/node/.config/fish \
97130ENV PATH="/home/node/.local/share/mise/shims:$PATH"
98131ENV MISE_TRUSTED_CONFIG_PATHS="/workspace"
99132
100- # ── Dev tools (always latest) ──────────────────────── ─────────────────────────
133+ # ── Dev tools (copied from parallel download stages) ─────────────────────────
101134# rtk (token-optimized CLI proxy) and ralphex (autonomous plan execution).
102135# Like Claude Code itself, these are dev infrastructure — not project dependencies.
103136# Downloaded from GitHub Releases; refreshed on each daily image rebuild.
104- USER root
105- RUN set -eux; \
106- ARCH="$(uname -m)" ; \
107- # ── rtk ──
108- case "$ARCH" in \
109- x86_64) RTK_TARGET="x86_64-unknown-linux-musl" ;; \
110- aarch64) RTK_TARGET="aarch64-unknown-linux-gnu" ;; \
111- esac; \
112- RTK_VERSION=$(curl -fsSL https://api.github.com/repos/rtk-ai/rtk/releases/latest | jq -r '.tag_name' | sed 's/^v//' ); \
113- curl -fsSL "https://github.com/rtk-ai/rtk/releases/download/v${RTK_VERSION}/rtk-${RTK_TARGET}.tar.gz" \
114- | tar -xz -C /usr/local/bin rtk; \
115- # ── ralphex ──
116- RALPHEX_ARCH=$(echo "$ARCH" | sed 's/x86_64/amd64/;s/aarch64/arm64/' ); \
117- RALPHEX_VERSION=$(curl -fsSL https://api.github.com/repos/umputun/ralphex/releases/latest | jq -r '.tag_name' | sed 's/^v//' ); \
118- curl -fsSL "https://github.com/umputun/ralphex/releases/download/v${RALPHEX_VERSION}/ralphex_${RALPHEX_VERSION}_linux_${RALPHEX_ARCH}.tar.gz" \
119- | tar -xz -C /usr/local/bin ralphex
120- USER node
137+ COPY --from=rtk-download /usr/local/bin/rtk /usr/local/bin/rtk
138+ COPY --from=ralphex-download /usr/local/bin/ralphex /usr/local/bin/ralphex
121139
122140# ── Playwright (headless shell for browser testing) ───────────────────────────
123141# Two-layer strategy for multi-project compatibility:
@@ -128,7 +146,9 @@ USER node
128146# at container creation to ensure the binary matches their @playwright/test.
129147# That command is idempotent: no-op when versions match, ~10s download if not.
130148USER root
131- RUN npx -y playwright@${PLAYWRIGHT_VERSION} install-deps chromium
149+ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
150+ --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
151+ npx -y playwright@${PLAYWRIGHT_VERSION} install-deps chromium
132152USER node
133153RUN npx -y playwright@${PLAYWRIGHT_VERSION} install --only-shell
134154ENV PLAYWRIGHT_VERSION=${PLAYWRIGHT_VERSION}
@@ -142,7 +162,8 @@ ENV PLAYWRIGHT_VERSION=${PLAYWRIGHT_VERSION}
142162# See: https://code.claude.com/docs/en/getting-started#install-with-npm
143163# Auto-updates don't matter here — the image rebuilds daily.
144164USER root
145- RUN npm install -g @anthropic-ai/claude-code
165+ RUN --mount=type=cache,target=/root/.npm \
166+ npm install -g @anthropic-ai/claude-code
146167USER node
147168
148169# ─── DEFAULT — full dev environment ───────────────────────────────────────────
@@ -161,29 +182,38 @@ USER node
161182# arm64: system Chromium (Chrome for Testing has no ARM64 Linux builds)
162183ARG AGENT_BROWSER_VERSION=latest
163184USER root
164- RUN npm install -g agent-browser@${AGENT_BROWSER_VERSION} \
185+ RUN --mount=type=cache,target=/root/.npm \
186+ --mount=type=cache,target=/var/cache/apt,sharing=locked \
187+ --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
188+ npm install -g agent-browser@${AGENT_BROWSER_VERSION} \
165189 && ARCH=$(dpkg --print-architecture) \
166190 && if [ "$ARCH" = "amd64" ]; then \
167191 agent-browser install --with-deps; \
168192 else \
169- apt-get update && apt-get install -y --no-install-recommends chromium \
170- && apt-get clean && rm -rf /var/lib/apt/lists/*; \
193+ apt-get update && apt-get install -y --no-install-recommends chromium; \
171194 fi \
172195 && chown -R node:node /usr/local/share/npm-global
173196USER node
174197
198+ LABEL org.opencontainers.image.source="https://github.com/gatezh/devcontainer-images" \
199+ org.opencontainers.image.description="Claude Code devcontainer — full dev environment with agent-browser, Playwright, and passwordless sudo" \
200+ org.opencontainers.image.licenses="MIT" \
201+ org.opencontainers.image.title="claude-code" \
202+ org.opencontainers.image.url="https://github.com/gatezh/devcontainer-images"
203+
175204# ─── SANDBOX — network-restricted environment ─────────────────────────────────
176205FROM base AS sandbox
177206
178207# Firewall packages (not needed in default target)
179208USER root
180- RUN apt-get update && apt-get install -y --no-install-recommends \
209+ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
210+ --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
211+ apt-get update && apt-get install -y --no-install-recommends \
181212 iptables \
182213 ipset \
183214 iproute2 \
184215 dnsutils \
185- aggregate \
186- && apt-get clean && rm -rf /var/lib/apt/lists/*
216+ aggregate
187217
188218# Firewall sudo rule for node user.
189219# The init-firewall.sh script is NOT baked into the image — each project
@@ -193,3 +223,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
193223RUN echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/node-firewall \
194224 && chmod 0440 /etc/sudoers.d/node-firewall
195225USER node
226+
227+ LABEL org.opencontainers.image.source="https://github.com/gatezh/devcontainer-images" \
228+ org.opencontainers.image.description="Claude Code devcontainer — network-restricted sandbox with firewall packages" \
229+ org.opencontainers.image.licenses="MIT" \
230+ org.opencontainers.image.title="claude-code-sandbox" \
231+ org.opencontainers.image.url="https://github.com/gatezh/devcontainer-images"
0 commit comments