From e40cfd6cb4ad67780167abbe37e61e979c587b9b Mon Sep 17 00:00:00 2001 From: gabemeola <14303404+gabemeola@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:49:37 +0000 Subject: [PATCH 01/12] Move build-essential, pkg-config, xz-utils to ephemeral build stage Restructure from three-stage to four-stage build: runtime - minimal apt (no compiler toolchain) build-env - runtime + build-essential/pkg-config/xz-utils builder - build-env + toolchains (unchanged) final - runtime + copied artifacts from builder Saves ~200 MB from the final image. Users who need native addon compilation can sudo apt install build-essential. --- Dockerfile | 31 ++++++++++++++++++------------- README.md | 2 +- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/Dockerfile b/Dockerfile index a2f57b9..9eba4d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,25 +7,24 @@ # lives under ~/workspace. # # Three-stage build: -# base — apt packages, user, sudo, init — shared by builder and final -# builder — fetches relocatable toolchains (opencode, Homebrew, mise) -# final — copies in runtimes from builder; carries only runtime layers +# runtime — minimal apt packages, user, sudo, init +# builder — runtime + compiler toolchain + relocatable toolchains (opencode, Homebrew, mise) +# final — copies in runtimes from builder; carries only runtime layers ARG OPENCODE_VERSION=0.0.0 ARG IMAGE_CREATED="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" # --------------------------------------------------------------------------- -# base: common runtime layer (apt, user, sudo, init) +# runtime: minimal runtime layer (apt, user, sudo, init) # --------------------------------------------------------------------------- -FROM ubuntu:26.04 AS base +FROM ubuntu:26.04 AS runtime ENV DEBIAN_FRONTEND=noninteractive -# General dev toolchain: VCS, build tools, languages, CLI utilities. +# CLI utilities for day-to-day dev work (git, curl, etc.). RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates curl git openssh-client unzip xz-utils \ - build-essential jq pkg-config \ - less sudo tini tzdata locales \ + ca-certificates curl git openssh-client unzip \ + jq less sudo tini tzdata locales \ && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb \ && userdel --remove ubuntu 2>/dev/null || true; \ groupdel ubuntu 2>/dev/null || true; \ @@ -40,12 +39,18 @@ COPY entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod 0755 /usr/local/bin/entrypoint.sh # --------------------------------------------------------------------------- -# builder: fetch relocatable toolchains (layers are ephemeral — -# only what's explicitly COPIED to final lands in the runtime image). +# builder: runtime + compiler toolchain + relocatable toolchains — +# only what's explicitly COPIED to final lands in the runtime image. # Order: most-stable first, so frequent version bumps don't bust the # cache of the other toolchains. # --------------------------------------------------------------------------- -FROM base AS builder +FROM runtime AS builder + +# Compiler toolchain needed for building native extensions during +# toolchain installation (Homebrew bottles, mise plugins, etc.). +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential pkg-config xz-utils \ + && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb # 1. Homebrew — the install script URL is stable; brew releases rarely # invalidate the layer once installed. @@ -86,7 +91,7 @@ RUN curl -fsSL https://opencode.ai/install | VERSION="${OPENCODE_VERSION}" bash # --------------------------------------------------------------------------- # final: runtime image — only the base layer plus copied-in toolchains # --------------------------------------------------------------------------- -FROM base +FROM runtime ARG OPENCODE_VERSION ARG IMAGE_CREATED diff --git a/README.md b/README.md index 88739d5..8123819 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A general-purpose Ubuntu Docker image for running [opencode](https://github.com/ | **Base OS** | ubuntu:26.04 | | **User** | `opencode` (uid/gid 1000), passwordless sudo | | **opencode** | Pinned in `version.txt` as `OPENCODE_VERSION` build arg | -| **Build tools** | `build-essential`, `pkg-config` (for native npm addons, pip source builds) | +| **Build tools** | Not included in runtime — `sudo apt install build-essential` if needed for native addons | | **Python 3** | Lazy-installed via mise (see table below) | | **Homebrew** | Linux-native Homebrew (`/home/linuxbrew/.linuxbrew`) — `brew` on PATH | | **zerobrew** | Faster Homebrew alternative (`zb` on PATH) -- used as mise backend for lazy-installed tools | From a95d31c597487dc798a3931fa4661799b4364a4d Mon Sep 17 00:00:00 2001 From: gabemeola <14303404+gabemeola@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:52:34 +0000 Subject: [PATCH 02/12] Drop locales from runtime (C.UTF-8 is sufficient) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9eba4d9..4ba8059 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ ENV DEBIAN_FRONTEND=noninteractive # CLI utilities for day-to-day dev work (git, curl, etc.). RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates curl git openssh-client unzip \ - jq less sudo tini tzdata locales \ + jq less sudo tini tzdata \ && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb \ && userdel --remove ubuntu 2>/dev/null || true; \ groupdel ubuntu 2>/dev/null || true; \ From f3375be4b81a96130a477341e6a3987620c92249 Mon Sep 17 00:00:00 2001 From: gabemeola <14303404+gabemeola@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:56:31 +0000 Subject: [PATCH 03/12] Move jq from apt to lazy-installed tools via mise --- Dockerfile | 2 +- README.md | 3 ++- mise-config.toml | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4ba8059..4a8ee42 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ ENV DEBIAN_FRONTEND=noninteractive # CLI utilities for day-to-day dev work (git, curl, etc.). RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates curl git openssh-client unzip \ - jq less sudo tini tzdata \ + less sudo tini tzdata \ && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb \ && userdel --remove ubuntu 2>/dev/null || true; \ groupdel ubuntu 2>/dev/null || true; \ diff --git a/README.md b/README.md index 8123819..0a643f9 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ A general-purpose Ubuntu Docker image for running [opencode](https://github.com/ | **Homebrew** | Linux-native Homebrew (`/home/linuxbrew/.linuxbrew`) — `brew` on PATH | | **zerobrew** | Faster Homebrew alternative (`zb` on PATH) -- used as mise backend for lazy-installed tools | | **mise** | Dev tool manager — tools listed below install on first use via `zerobrew` backend | -| **CLI utilities** | git, curl, jq, less, unzip, ssh client | +| **CLI utilities** | git, curl, less, unzip, ssh client | | **Init** | tini as PID 1 (zombie reaping, clean shutdown) | ### Lazy-installed tools @@ -29,6 +29,7 @@ These tools install on first use (via mise → github/zerobrew): | Tool | Command | Backend | |---|---|---| | GitHub CLI | `gh` | github | +| jq | `jq` | github | | GitLab CLI | `glab` | zerobrew | | Ruby | `ruby` | zerobrew | | ripgrep | `rg` | github | diff --git a/mise-config.toml b/mise-config.toml index 646d0aa..3f3a6a5 100644 --- a/mise-config.toml +++ b/mise-config.toml @@ -1,5 +1,6 @@ [tools] "github:cli/cli" = "latest" # shim:gh +"github:jqlang/jq" = "latest" "zerobrew:glab" = "latest" "zerobrew:ruby" = "latest" "github:BurntSushi/ripgrep" = "latest" # shim:rg From 194e8158d73d1a4c4b619d6529ba1b6e78254423 Mon Sep 17 00:00:00 2001 From: gabemeola <14303404+gabemeola@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:59:41 +0000 Subject: [PATCH 04/12] Add libatomic1 to runtime (needed by official Node.js binaries on arm64) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4a8ee42..51b327c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ ENV DEBIAN_FRONTEND=noninteractive # CLI utilities for day-to-day dev work (git, curl, etc.). RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates curl git openssh-client unzip \ - less sudo tini tzdata \ + less libatomic1 sudo tini tzdata \ && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb \ && userdel --remove ubuntu 2>/dev/null || true; \ groupdel ubuntu 2>/dev/null || true; \ From baadc0c0dece64a0021e755034fb49b8353f0e94 Mon Sep 17 00:00:00 2001 From: gabemeola <14303404+gabemeola@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:02:43 +0000 Subject: [PATCH 05/12] Switch node from github:nodejs/node to core plugin (supports arm64) --- README.md | 2 +- mise-config.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0a643f9..ac86fed 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ These tools install on first use (via mise → github/zerobrew): | Micro | `micro` | github | | Nano | `nano` | zerobrew | | Python 3 | `python3` | zerobrew | -| Node.js | `node` | github | +| Node.js | `node` | core / zerobrew | | Sapling | `sl` | github | The image ships with a system config at `/etc/mise/config.toml` with these pre-approved tools. Users can add or override tools by creating `~/.config/mise/config.toml` — mise merges both. diff --git a/mise-config.toml b/mise-config.toml index 3f3a6a5..21c3c45 100644 --- a/mise-config.toml +++ b/mise-config.toml @@ -10,5 +10,5 @@ "github:zyedidia/micro" = "latest" "zerobrew:nano" = "latest" "zerobrew:python" = "latest" -"github:nodejs/node" = "latest" +"node" = "latest" "github:facebook/sapling" = "latest" # shim:sl From f4110fe204785682573ae8cb867a7c15214ff4ec Mon Sep 17 00:00:00 2001 From: gabemeola <14303404+gabemeola@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:03:11 +0000 Subject: [PATCH 06/12] Fix node backend in table: core, not zerobrew --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ac86fed..bc23c60 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ These tools install on first use (via mise → github/zerobrew): | Micro | `micro` | github | | Nano | `nano` | zerobrew | | Python 3 | `python3` | zerobrew | -| Node.js | `node` | core / zerobrew | +| Node.js | `node` | core | | Sapling | `sl` | github | The image ships with a system config at `/etc/mise/config.toml` with these pre-approved tools. Users can add or override tools by creating `~/.config/mise/config.toml` — mise merges both. From 00a6b6f51f6d5a6d4ec5bf630d94c3e80390b8dc Mon Sep 17 00:00:00 2001 From: Gabe M Date: Thu, 2 Jul 2026 12:05:16 -0600 Subject: [PATCH 07/12] chore: update readme --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index bc23c60..82e1898 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,7 @@ A general-purpose Ubuntu Docker image for running [opencode](https://github.com/ | **Base OS** | ubuntu:26.04 | | **User** | `opencode` (uid/gid 1000), passwordless sudo | | **opencode** | Pinned in `version.txt` as `OPENCODE_VERSION` build arg | -| **Build tools** | Not included in runtime — `sudo apt install build-essential` if needed for native addons | -| **Python 3** | Lazy-installed via mise (see table below) | +| **Lazy Installed Tools** | Node, Python3 (see below) | | **Homebrew** | Linux-native Homebrew (`/home/linuxbrew/.linuxbrew`) — `brew` on PATH | | **zerobrew** | Faster Homebrew alternative (`zb` on PATH) -- used as mise backend for lazy-installed tools | | **mise** | Dev tool manager — tools listed below install on first use via `zerobrew` backend | From d02630fe985a094f5cf4d8acd960cb71ffa8d442 Mon Sep 17 00:00:00 2001 From: Gabe M Date: Thu, 2 Jul 2026 12:09:13 -0600 Subject: [PATCH 08/12] chore: copy layer order --- Dockerfile | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 51b327c..fc6a6c3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -115,10 +115,8 @@ LABEL io.artifacthub.package.readme-url="https://raw.githubusercontent.com/spris io.artifacthub.package.maintainers='[{"name":"Gabriel Meola","email":"banter@gabe.mx"}]' \ io.artifacthub.package.keywords="opencode,server,docker,ai,code,editor,development" -# Runtimes copied from builder (most-stable first so frequent version -# bumps don't invalidate cache for the other layers). -COPY --from=builder --chown=opencode:opencode /home/linuxbrew /home/linuxbrew -COPY --from=builder /opt/opencode /usr/local/bin/opencode +# Copy layers +# Ordered by most stable layers first so cache can be reused. # Mise — dev tool manager; auto-installs tools defined in the global config. COPY --from=builder /usr/local/bin/mise /usr/local/bin/mise @@ -130,6 +128,10 @@ COPY --from=builder /home/opencode/.local/bin/zb /usr/local/bin/zb COPY --from=builder /home/opencode/.local/bin/zbx /usr/local/bin/zbx COPY --from=builder --chown=opencode:opencode /home/opencode/.local/share/zerobrew /home/opencode/.local/share/zerobrew +# Opencode +COPY --from=builder --chown=opencode:opencode /home/linuxbrew /home/linuxbrew +COPY --from=builder /opt/opencode /usr/local/bin/opencode + # Verify runtime and set up login-shell PATH and auto-install handler RUN opencode --version \ && printf 'for d in "$HOME/.local/bin" "/home/linuxbrew/.linuxbrew/bin" "/home/linuxbrew/.linuxbrew/sbin" "$HOME/.local/share/zerobrew/prefix/bin"; do case ":$PATH:" in *":$d:"*) ;; *) PATH="$d:$PATH";; esac; done\nexport PATH\n' > /etc/profile.d/brew-path.sh \ From 12e585bdc4a937db89b39be57e2658a68df2e165 Mon Sep 17 00:00:00 2001 From: gabemeola <14303404+gabemeola@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:14:08 +0000 Subject: [PATCH 09/12] Use partial clone for Homebrew; strip docs/logs from apt layer - git clone --filter=blob:none for Homebrew saves ~70 MB without breaking brew update (partial clone fetches blobs on demand) - Remove /usr/share/doc, /usr/share/man, /usr/share/locale, /var/log/*, and /var/cache/debconf/*.dat from apt install layer to trim the 100 MB apt layer --- Dockerfile | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index fc6a6c3..61a91c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates curl git openssh-client unzip \ less libatomic1 sudo tini tzdata \ && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb \ + && rm -rf /usr/share/doc /usr/share/man /usr/share/locale \ + && find /var/log -type f -delete 2>/dev/null; \ + rm -f /var/cache/debconf/*.dat 2>/dev/null || true \ && userdel --remove ubuntu 2>/dev/null || true; \ groupdel ubuntu 2>/dev/null || true; \ groupadd --gid 1000 opencode \ @@ -52,11 +55,16 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential pkg-config xz-utils \ && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb -# 1. Homebrew — the install script URL is stable; brew releases rarely -# invalidate the layer once installed. +# 1. Homebrew — partial clone with --filter=blob:none avoids downloading +# all past file versions, saving ~70 MB while keeping brew update working. RUN mkdir -p /home/linuxbrew \ && chown opencode:opencode /home/linuxbrew \ - && sudo -u opencode NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" \ + && sudo -u opencode git clone --filter=blob:none \ + https://github.com/Homebrew/brew /home/linuxbrew/.linuxbrew/Homebrew \ + && sudo -u opencode mkdir -p /home/linuxbrew/.linuxbrew/bin \ + && sudo -u opencode ln -sf \ + /home/linuxbrew/.linuxbrew/Homebrew/bin/brew /home/linuxbrew/.linuxbrew/bin/brew \ + && sudo -u opencode /home/linuxbrew/.linuxbrew/bin/brew update --force \ && sudo -u opencode /home/linuxbrew/.linuxbrew/bin/brew cleanup --prune=all \ && sudo -u opencode rm -rf "$(sudo -u opencode /home/linuxbrew/.linuxbrew/bin/brew --cache)" \ && rm -rf /home/linuxbrew/.linuxbrew/Homebrew/Library/Homebrew/test \ From feb673c9d4039c335ca8fdf197cea0dd529ed353 Mon Sep 17 00:00:00 2001 From: Gabe M Date: Thu, 2 Jul 2026 12:19:43 -0600 Subject: [PATCH 10/12] chore: update copy order --- Dockerfile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 61a91c4..128cfd8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -125,19 +125,19 @@ LABEL io.artifacthub.package.readme-url="https://raw.githubusercontent.com/spris # Copy layers # Ordered by most stable layers first so cache can be reused. - -# Mise — dev tool manager; auto-installs tools defined in the global config. -COPY --from=builder /usr/local/bin/mise /usr/local/bin/mise -COPY --from=builder --chown=opencode:opencode /opt/mise /opt/mise -COPY mise-config.toml /etc/mise/config.toml +COPY --from=builder --chown=opencode:opencode /home/linuxbrew /home/linuxbrew # Zerobrew — fast Homebrew alternative; mise zerobrew backend. COPY --from=builder /home/opencode/.local/bin/zb /usr/local/bin/zb COPY --from=builder /home/opencode/.local/bin/zbx /usr/local/bin/zbx COPY --from=builder --chown=opencode:opencode /home/opencode/.local/share/zerobrew /home/opencode/.local/share/zerobrew +# Mise — dev tool manager; auto-installs tools defined in the global config. +COPY --from=builder /usr/local/bin/mise /usr/local/bin/mise +COPY --from=builder --chown=opencode:opencode /opt/mise /opt/mise +COPY mise-config.toml /etc/mise/config.toml + # Opencode -COPY --from=builder --chown=opencode:opencode /home/linuxbrew /home/linuxbrew COPY --from=builder /opt/opencode /usr/local/bin/opencode # Verify runtime and set up login-shell PATH and auto-install handler From 362603865637d5bc3d953159809bdb8afa503738 Mon Sep 17 00:00:00 2001 From: gabemeola <14303404+gabemeola@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:22:06 +0000 Subject: [PATCH 11/12] =?UTF-8?q?Fix=20apt=20cleanup=20order=20=E2=80=94?= =?UTF-8?q?=20run=20log/debconf=20cleanup=20after=20all=20commands?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 128cfd8..073da95 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,15 +27,15 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ less libatomic1 sudo tini tzdata \ && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb \ && rm -rf /usr/share/doc /usr/share/man /usr/share/locale \ - && find /var/log -type f -delete 2>/dev/null; \ - rm -f /var/cache/debconf/*.dat 2>/dev/null || true \ && userdel --remove ubuntu 2>/dev/null || true; \ groupdel ubuntu 2>/dev/null || true; \ groupadd --gid 1000 opencode \ && useradd --uid 1000 --gid 1000 --create-home --shell /bin/bash opencode \ && echo 'opencode ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/opencode \ && chmod 0440 /etc/sudoers.d/opencode \ - && visudo -cf /etc/sudoers.d/opencode + && visudo -cf /etc/sudoers.d/opencode \ + && find /var/log -type f -delete 2>/dev/null; \ + rm -f /var/cache/debconf/*.dat 2>/dev/null || true # Entrypoint (tini + init script) COPY entrypoint.sh /usr/local/bin/entrypoint.sh From 1ec7a935fc1bee2b0b82ccdc4123d15626815ea1 Mon Sep 17 00:00:00 2001 From: gabemeola <14303404+gabemeola@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:25:43 +0000 Subject: [PATCH 12/12] Remove openssh-client from runtime (not needed) --- Dockerfile | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 073da95..38ad14b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,7 +23,7 @@ ENV DEBIAN_FRONTEND=noninteractive # CLI utilities for day-to-day dev work (git, curl, etc.). RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates curl git openssh-client unzip \ + ca-certificates curl git unzip \ less libatomic1 sudo tini tzdata \ && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb \ && rm -rf /usr/share/doc /usr/share/man /usr/share/locale \ diff --git a/README.md b/README.md index 82e1898..26de77e 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ A general-purpose Ubuntu Docker image for running [opencode](https://github.com/ | **Homebrew** | Linux-native Homebrew (`/home/linuxbrew/.linuxbrew`) — `brew` on PATH | | **zerobrew** | Faster Homebrew alternative (`zb` on PATH) -- used as mise backend for lazy-installed tools | | **mise** | Dev tool manager — tools listed below install on first use via `zerobrew` backend | -| **CLI utilities** | git, curl, less, unzip, ssh client | +| **CLI utilities** | git, curl, less, unzip | | **Init** | tini as PID 1 (zombie reaping, clean shutdown) | ### Lazy-installed tools