From 36a7daab4cdbd60df7a0f3dabc529cacf42dcbaf Mon Sep 17 00:00:00 2001 From: Niklaus Bucher Date: Sun, 18 Oct 2026 02:36:54 +0200 Subject: [PATCH 1/2] fix: trim OpenCode v1 layers from image --- Dockerfile | 20 ++++++++++++++------ README.md | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1b85d32..33e1993 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,13 @@ -# Docker Sandboxes template: pinned OpenCode v2 on the opencode-docker variant. +# Docker Sandboxes template: pinned OpenCode v2 on the shell-docker variant. # Build: docker build -t sbx-opencode2:local . # Bump: Renovate opens PRs for the base image digest and OPENCODE_VERSION; # merging to main publishes the version-tagged image. +# shell-docker is opencode-docker without the OpenCode V1 layer (~520 MB +# compressed). An npm uninstall on top of opencode-docker would only add +# whiteouts; the V1 layer would still ship in the image. # renovate: datasource=docker depName=docker/sandbox-templates -FROM docker/sandbox-templates:opencode-docker@sha256:ef636291701f88bbe1a268bd083175804a84309c14faf75bc8c2ed179ce61db3 +FROM docker/sandbox-templates:shell-docker@sha256:1560168ac5fb9ce23d413c878349334c5845c07e264cd675d7867f0c78ad1761 # renovate: datasource=npm depName=@opencode/cli ARG OPENCODE_VERSION=2.0.32 @@ -13,13 +16,18 @@ LABEL org.opencontainers.image.source=https://github.com/nikbucher/sbx-opencode2 LABEL org.opencontainers.image.description="Docker Sandboxes template with a pinned OpenCode v2 release" LABEL org.opencontainers.image.licenses=MIT -# The base image ships OpenCode V1 (npm package "opencode-ai"). -# Replace it with a pinned V2 release from the @opencode/cli package. +# Install a pinned V2 release from the @opencode/cli package. # Run the install as agent, not root: the base image's NPM_CONFIG_PREFIX # is agent-owned, and agent-owned package files are what make in-sandbox # self-updates (/update, opencode upgrade) work at all. +# The platform packages carry no libc field, so npm installs several linux +# variants (glibc/musl x baseline, ~200 MB each) even with --omit=optional. +# The postinstall already put the matching binary into bin/, so drop the +# nested platform packages afterwards. USER agent -RUN npm uninstall -g opencode-ai \ - && npm install -g "@opencode/cli@${OPENCODE_VERSION}" \ +RUN npm install -g --omit=optional "@opencode/cli@${OPENCODE_VERSION}" \ + && rm -rf "$(npm root -g)/@opencode/cli/node_modules" \ && npm cache clean --force \ && opencode --version + +CMD ["opencode"] diff --git a/README.md b/README.md index 9cdcd89..f22ba45 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Run OpenCode v2 in [Docker Sandboxes](https://docs.docker.com/ai/sandboxes/customize/templates/) with a consistent starting version, on amd64 and arm64. -The base template ships OpenCode V1; this image swaps in the pinned V2 CLI, so every sandbox starts with the exact same OpenCode release. `/update` inside a running sandbox upgrades only that sandbox — the durable path to a newer OpenCode is a newer image tag. +The image builds on the `shell-docker` sandbox template and adds the pinned OpenCode V2 CLI, so every sandbox starts with the exact same OpenCode release. `/update` inside a running sandbox upgrades only that sandbox — the durable path to a newer OpenCode is a newer image tag. ## Use it From 038f28430d65d9f46c6a1d4a2a58f3bccb0a8a5e Mon Sep 17 00:00:00 2001 From: Niklaus Bucher Date: Sun, 18 Oct 2026 02:46:00 +0200 Subject: [PATCH 2/2] fix: install platform binaries and smoke-test both architectures --- .github/workflows/publish.yml | 52 ++++++++++++++++++++++++++++++----- CONTRIBUTING.md | 2 +- Dockerfile | 13 +++++---- README.md | 2 +- 4 files changed, 55 insertions(+), 14 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1271e87..eb45ada 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -110,8 +110,9 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - # Smoke test: build the amd64 image and verify the pinned OpenCode - # version runs before anything is published. + # Smoke test: build each platform image and verify the pinned OpenCode + # version runs before anything is published. arm64 runs under QEMU; + # both builds warm the cache for the multi-arch push below. - name: Build amd64 image for smoke test if: steps.changes.outputs.build == 'true' uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 @@ -119,7 +120,18 @@ jobs: context: . platforms: linux/amd64 load: true - tags: sbx-opencode2:smoke + tags: sbx-opencode2:smoke-amd64 + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Build arm64 image for smoke test + if: steps.changes.outputs.build == 'true' + uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 + with: + context: . + platforms: linux/arm64 + load: true + tags: sbx-opencode2:smoke-arm64 cache-from: type=gha cache-to: type=gha,mode=max @@ -127,15 +139,41 @@ jobs: if: steps.changes.outputs.build == 'true' run: | EXPECTED=$(sed -n 's/^ARG OPENCODE_VERSION=//p' Dockerfile) - ACTUAL=$(docker run --rm sbx-opencode2:smoke opencode --version) - echo "expected=v${EXPECTED} actual=${ACTUAL}" - echo "${ACTUAL}" | grep -q "v${EXPECTED}" + for arch in amd64 arm64; do + ACTUAL=$(docker run --rm --platform "linux/${arch}" "sbx-opencode2:smoke-${arch}" opencode --version) + echo "${arch}: expected=v${EXPECTED} actual=${ACTUAL}" + echo "${ACTUAL}" | grep -q "v${EXPECTED}" + done # The npm prefix is agent-owned in the base image; installing as root # would leave the package root-owned and break in-sandbox self-updates. - name: Smoke test (package writable by agent) if: steps.changes.outputs.build == 'true' - run: docker run --rm sbx-opencode2:smoke sh -c 'test -w "$NPM_CONFIG_PREFIX/lib/node_modules/@opencode/cli"' + run: | + for arch in amd64 arm64; do + docker run --rm --platform "linux/${arch}" "sbx-opencode2:smoke-${arch}" sh -c 'test -w "$NPM_CONFIG_PREFIX/lib/node_modules/@opencode/cli"' + done + + # The package nests a ~200 MB binary per libc variant; the Dockerfile + # drops them after the postinstall. Fail if they come back, e.g. after + # an upstream layout change makes the rm miss them. One binary is + # ~200 MB, so 300 MB leaves headroom without hiding a second one. + - name: Smoke test (no nested platform binaries) + if: steps.changes.outputs.build == 'true' + run: | + for arch in amd64 arm64; do + docker run --rm --platform "linux/${arch}" "sbx-opencode2:smoke-${arch}" sh -euc ' + pkg="$NPM_CONFIG_PREFIX/lib/node_modules/@opencode" + for dir in "$pkg"/cli/node_modules/@opencode/cli-* "$pkg"/cli-*; do + if [ -e "$dir" ]; then + echo "platform package present: $dir"; exit 1 + fi + done + size=$(du -sm "$pkg" | cut -f1) + echo "'"${arch}"': @opencode is ${size} MB" + test "$size" -lt 300 + ' + done # Tag published images with the pinned OpenCode version, so an # automerged Renovate bump releases itself without a manual git tag. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e93bdd1..7c93afd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ ## CI: build and publish -`.github/workflows/publish.yml` runs on every PR and every push to `main`. It detects build-relevant changes (`Dockerfile`, `.dockerignore`, the workflow itself), comparing push endpoints with a two-dot diff and PRs against the merge base with a three-dot diff. Relevant changes trigger an amd64 smoke test — the OpenCode version must match the `Dockerfile` and the npm prefix must be writable by `agent` — and a multi-arch (amd64 + arm64) build. PRs build and test without publishing. Only relevant pushes to `main` and manual runs on `main` publish to GHCR and Docker Hub with provenance attestations, tagged with `OPENCODE_VERSION` from the `Dockerfile` and `latest`. +`.github/workflows/publish.yml` runs on every PR and every push to `main`. It detects build-relevant changes (`Dockerfile`, `.dockerignore`, the workflow itself), comparing push endpoints with a two-dot diff and PRs against the merge base with a three-dot diff. Relevant changes trigger amd64 and arm64 smoke tests (arm64 under QEMU) — the OpenCode version must match the `Dockerfile`, the npm prefix must be writable by `agent`, and no nested platform binaries may remain — and a multi-arch (amd64 + arm64) build. PRs build and test without publishing. Only relevant pushes to `main` and manual runs on `main` publish to GHCR and Docker Hub with provenance attestations, tagged with `OPENCODE_VERSION` from the `Dockerfile` and `latest`. Docs-only changes skip the build but the required `build-and-push` job still succeeds. Manual runs (`workflow_dispatch`) always build; runs on other branches do not publish. If the comparison base is unavailable (new branch, missing commit), the build runs conservatively. Actual diff errors fail the job rather than reporting a successful skip. diff --git a/Dockerfile b/Dockerfile index 33e1993..0c98950 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,14 +20,17 @@ LABEL org.opencontainers.image.licenses=MIT # Run the install as agent, not root: the base image's NPM_CONFIG_PREFIX # is agent-owned, and agent-owned package files are what make in-sandbox # self-updates (/update, opencode upgrade) work at all. -# The platform packages carry no libc field, so npm installs several linux -# variants (glibc/musl x baseline, ~200 MB each) even with --omit=optional. -# The postinstall already put the matching binary into bin/, so drop the -# nested platform packages afterwards. +# The platform binaries are optionalDependencies without a libc field, so +# npm installs both the glibc and musl variant (~200 MB each). The package +# has no other dependencies, and the postinstall hard-links (or copies) the +# matching binary into bin/, so its nested node_modules can go entirely. +# The empty config dir matches opencode-docker, which ships it agent-owned. USER agent -RUN npm install -g --omit=optional "@opencode/cli@${OPENCODE_VERSION}" \ +RUN npm install -g "@opencode/cli@${OPENCODE_VERSION}" \ && rm -rf "$(npm root -g)/@opencode/cli/node_modules" \ && npm cache clean --force \ + && mkdir -p "$HOME/.config/opencode" \ && opencode --version +# shell-docker defaults to bash; restore opencode-docker's default command. CMD ["opencode"] diff --git a/README.md b/README.md index f22ba45..e06c165 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Run OpenCode v2 in [Docker Sandboxes](https://docs.docker.com/ai/sandboxes/customize/templates/) with a consistent starting version, on amd64 and arm64. -The image builds on the `shell-docker` sandbox template and adds the pinned OpenCode V2 CLI, so every sandbox starts with the exact same OpenCode release. `/update` inside a running sandbox upgrades only that sandbox — the durable path to a newer OpenCode is a newer image tag. +The image builds on the `shell-docker` sandbox template and adds the pinned OpenCode V2 CLI, so every sandbox starts with the exact same OpenCode release. `/update` inside a running sandbox upgrades only that sandbox — the durable path to a newer OpenCode is a newer image tag. The image keeps only the binary for its platform; an in-sandbox update reinstalls the unused libc variant too, adding ~200 MB to that sandbox. ## Use it