Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c78500f
feat: replace custom auto-install shim with mise for on-demand tools
gabemeola Jun 24, 2026
f8336f9
fix: use correct MISE_INSTALL_PATH env var, pre-install gh/glab via b…
gabemeola Jun 24, 2026
3c9f88f
fix: use HOMEBREW_INSTALL_FROM_API=1, strip entire core tap
gabemeola Jun 24, 2026
1aec935
fix: restore full homebrew-core tap so brew update works at runtime
gabemeola Jun 24, 2026
493299f
fix: re-enable HOMEBREW_INSTALL_FROM_API=1, strip core tap
gabemeola Jun 24, 2026
d2df34c
feat: move ruby, jq, ripgrep, fd, wget, vim, nano to lazy install via…
gabemeola Jun 24, 2026
934b733
chore: remove pre-install of gh/glab, all tools now lazy via mise
gabemeola Jun 24, 2026
9d383f2
fix: move mise config to user home dir
gabemeola Jun 24, 2026
feaf738
feat: move python3 to lazy install via mise
gabemeola Jun 24, 2026
4e8cf09
chore: remove n and node from mise config (already pre-installed)
gabemeola Jun 24, 2026
5f08743
Replace pre-installed Node.js (n) with lazy mise install; remove open…
gabemeola Jun 24, 2026
5330397
Fix negative image diff: pass abs value to numfmt, handle sign separa…
gabemeola Jun 24, 2026
02a7ba4
Move mise config to /etc/mise/config.toml (system config, outside PVC)
gabemeola Jun 24, 2026
81d68cd
Install mise brew backend plugin (brew:tool prefix requires it)
gabemeola Jun 24, 2026
085cf5a
Pre-install jq (brew plugin dependency); restore Homebrew cask (brew …
gabemeola Jun 24, 2026
9ac1a58
Switch mise activation to shims mode (hook-not-found doesn't match pr…
gabemeola Jun 24, 2026
e428612
Add mise activation for zsh, fish, and sh shells
gabemeola Jun 24, 2026
36424da
Create mise shims for each configured tool (mise exec handles auto-in…
gabemeola Jun 24, 2026
47378b4
Use --shims mode so mise hook-env adds /opt/mise/shims to PATH (shim …
gabemeola Jun 24, 2026
51ae32e
Fix shim format: add tool name after -- so args pass correctly; add s…
gabemeola Jun 24, 2026
6306cea
Move shims after brew bins in PATH; use plain mise activate (no --shims)
gabemeola Jun 24, 2026
5181c8e
Move auto-install shims outside MISE_DATA_DIR so mise hook-env doesn'…
gabemeola Jun 24, 2026
4fc192d
Add micro editor to lazy-installed tools
gabemeola Jun 24, 2026
0156979
Remove n from lazy tools (mise manages node versions now)
gabemeola Jun 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions .github/workflows/pr-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,14 @@ jobs:
BASELINE_HUMAN="${{ steps.baseline-size.outputs.human }}"
BASELINE_BYTES=${{ steps.baseline-size.outputs.bytes }}
DIFF_BYTES=$(( PR_BYTES - BASELINE_BYTES ))
if [ "$DIFF_BYTES" -ge 0 ]; then SIGN="+"; else SIGN=""; fi
DIFF_HUMAN=$(numfmt --to=iec --suffix=B "$DIFF_BYTES" 2>/dev/null || echo "${DIFF_BYTES} B")
if [ "$DIFF_BYTES" -gt 0 ]; then
SIGN="+"
ABS_DIFF=$DIFF_BYTES
else
SIGN=""
ABS_DIFF=$(( -DIFF_BYTES ))
fi
DIFF_HUMAN=$(numfmt --to=iec --suffix=B "$ABS_DIFF" 2>/dev/null || echo "${ABS_DIFF} B")
fi

{
Expand Down Expand Up @@ -121,11 +127,7 @@ jobs:
echo "pr: $PR_HUMAN ($PR_BYTES bytes)"

echo "pr-human=${PR_HUMAN}" >> "$GITHUB_OUTPUT"
if [ "$DIFF_BYTES" -ge 0 ]; then
echo "diff-human=+${DIFF_HUMAN}" >> "$GITHUB_OUTPUT"
else
echo "diff-human=${DIFF_HUMAN}" >> "$GITHUB_OUTPUT"
fi
echo "diff-human=${SIGN}${DIFF_HUMAN}" >> "$GITHUB_OUTPUT"

- name: Comment on PR
if: github.event_name == 'pull_request'
Expand Down
81 changes: 41 additions & 40 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@
# Ubuntu base with a broad, familiar dev toolchain. All configuration is env-driven
# (OPENCODE_SERVER_PASSWORD, OPENCODE_CORS_ORIGIN, OPENCODE_PORT). The entire home
# directory (/home/opencode) is the persistent mount point; the active project
# lives under ~/workspace. Node lives in /opt/n (outside home) so it's never
# shadowed when a volume mounts over the home directory.
# lives under ~/workspace.
#
# Three-stage build:
# base — apt packages, user, sudo, init — shared by builder and final
# builder — fetches relocatable toolchains (Node, opencode, Homebrew)
# builder — fetches relocatable toolchains (opencode, Homebrew, mise)
# final — copies in runtimes from builder; carries only runtime layers

ARG OPENCODE_VERSION=0.0.0
ARG NODE_PREFIX=/opt/n

# ---------------------------------------------------------------------------
# base: common runtime layer (apt, user, sudo, init)
Expand All @@ -23,20 +21,11 @@ FROM ubuntu:26.04 AS base
ENV DEBIAN_FRONTEND=noninteractive

# General dev toolchain: VCS, build tools, languages, CLI utilities.
# Also installs GitHub CLI via its official apt repo.
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl wget git openssh-client unzip xz-utils \
build-essential pkg-config \
python3 python3-pip python3-venv ruby \
ripgrep fd-find jq less nano vim-tiny \
sudo tini open-iscsi tzdata locales \
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
| dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends gh \
&& rm -rf /var/lib/apt/lists/* \
ca-certificates curl git openssh-client unzip xz-utils \
build-essential jq pkg-config \
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; \
groupadd --gid 1000 opencode \
Expand All @@ -57,37 +46,30 @@ RUN chmod 0755 /usr/local/bin/entrypoint.sh
# ---------------------------------------------------------------------------
FROM base AS builder

ARG NODE_PREFIX
ENV N_PREFIX=${NODE_PREFIX}
ENV PATH=${NODE_PREFIX}/bin:${PATH}

# 1. Homebrew — the install script URL is stable; brew releases rarely
# invalidate the layer once installed.
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 /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/Taps/homebrew/homebrew-core \
&& rm -rf /home/linuxbrew/.linuxbrew/Homebrew/Library/Homebrew/test \
&& rm -rf /home/linuxbrew/.linuxbrew/Homebrew/Library/Homebrew/cask \
&& rm -rf /home/linuxbrew/.linuxbrew/Homebrew/Library/Homebrew/vendor/bundle/ruby/*/cache \
&& rm -rf /home/linuxbrew/.linuxbrew/Homebrew/Library/Homebrew/vendor/bundle/ruby/*/doc \
&& rm -rf /home/linuxbrew/.linuxbrew/Homebrew/Library/Homebrew/vendor/portable-ruby \
&& rm -rf /home/linuxbrew/.linuxbrew/share/man \
&& rm -rf /home/linuxbrew/.linuxbrew/share/doc \
&& rm -rf /home/linuxbrew/.linuxbrew/share/zsh
&& rm -rf /home/linuxbrew/.linuxbrew/share/zsh \
&& rm -rf /home/linuxbrew/.linuxbrew/Homebrew/Library/Taps/homebrew/homebrew-core

# 2. Node.js via `n` — changes when the upstream LTS version bumps
RUN curl -fsSL -o /usr/local/bin/n https://raw.githubusercontent.com/tj/n/master/bin/n \
&& chmod 0755 /usr/local/bin/n \
&& mkdir -p "${N_PREFIX}" \
&& n install --cleanup current \
&& node --version && npm --version
# 1.5. mise — dev tool manager; pre-approved tools defined in the global config
# auto-install via Homebrew backend on first use at runtime.
RUN curl -fsSL https://mise.run | MISE_INSTALL_PATH=/usr/local/bin/mise sh \
&& mkdir -p /opt/mise \
&& MISE_DATA_DIR=/opt/mise mise plugins install brew https://github.com/woutermont/mise-brew

ARG OPENCODE_VERSION

# 3. opencode server binary — changes on every version bump (most frequent)
# 2. opencode server binary — changes on every version bump (most frequent)
RUN curl -fsSL https://opencode.ai/install | VERSION="${OPENCODE_VERSION}" bash \
&& (cp /root/.opencode/bin/opencode /opt/opencode 2>/dev/null \
|| cp "$HOME/.opencode/bin/opencode" /opt/opencode) \
Expand All @@ -99,20 +81,39 @@ RUN curl -fsSL https://opencode.ai/install | VERSION="${OPENCODE_VERSION}" bash
# ---------------------------------------------------------------------------
FROM base

ARG NODE_PREFIX
ENV N_PREFIX=${NODE_PREFIX}
ENV PATH=${N_PREFIX}/bin:/home/opencode/.local/bin:/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:${PATH}
ENV PATH=/home/opencode/.local/bin:/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:/opt/auto-install-shims:${PATH}
ENV HOMEBREW_NO_AUTO_UPDATE=1
ENV HOMEBREW_INSTALL_FROM_API=1
ENV MISE_DATA_DIR=/opt/mise
ENV MISE_ALWAYS_INSTALL=1

# 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 --chown=opencode:opencode ${NODE_PREFIX} ${NODE_PREFIX}
COPY --from=builder /opt/opencode /usr/local/bin/opencode

# Verify runtimes and set up login-shell PATH
RUN node --version && npm --version && opencode --version \
&& printf 'export N_PREFIX=%s\nfor d in "$N_PREFIX/bin" "$HOME/.local/bin" "/home/linuxbrew/.linuxbrew/bin" "/home/linuxbrew/.linuxbrew/sbin"; do case ":$PATH:" in *":$d:"*) ;; *) PATH="$d:$PATH";; esac; done\nexport PATH\n' "${N_PREFIX}" > /etc/profile.d/node-path.sh \
&& chmod 0644 /etc/profile.d/node-path.sh
# 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

# 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"; do case ":$PATH:" in *":$d:"*) ;; *) PATH="$d:$PATH";; esac; done\nexport PATH\n' > /etc/profile.d/brew-path.sh \
&& chmod 0644 /etc/profile.d/brew-path.sh \
&& printf '\neval "$(mise activate bash)"\n' >> /home/opencode/.bashrc \
&& printf '\neval "$(mise activate zsh)"\n' >> /home/opencode/.zshrc \
&& mkdir -p /home/opencode/.config/fish \
&& printf '\nmise activate fish | source\n' >> /home/opencode/.config/fish/config.fish \
&& printf '\neval "$(mise activate sh)"\n' >> /home/opencode/.profile \
&& mkdir -p /opt/auto-install-shims \
&& grep -E '^\s*"' /etc/mise/config.toml | while IFS='=' read -r key value; do \
key="$(echo "$key" | tr -d ' "')" \
&& shim="${key#*:}" \
&& printf '#!/usr/bin/env bash\nexec /usr/local/bin/mise exec "%s" -- %s "$@"\n' "$key" "$shim" > "/opt/auto-install-shims/$shim" \
&& chmod 0755 "/opt/auto-install-shims/$shim"; \
done \
&& chown -R opencode:opencode /opt/auto-install-shims

USER opencode
ENV HOME=/home/opencode
Expand Down
31 changes: 26 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,33 @@ A general-purpose Ubuntu Docker image for running [opencode](https://opencode.ai
| **Base OS** | ubuntu:26.04 |
| **User** | `opencode` (uid/gid 1000), passwordless sudo |
| **opencode** | Pinned in `version.txt` as `OPENCODE_VERSION` build arg |
| **Node.js** | Current LTS via `n`, installed to `/opt/n` (outside home) |
| **Python 3** | pip, venv |
| **Build tools** | `build-essential`, `pkg-config` (for native npm addons, pip source builds) |
| **Homebrew** | Linux-native Homebrew (`/home/linuxbrew/.linuxbrew`) — `brew` on PATH for all users |
| **CLI utilities** | git, curl, wget, gh (GitHub CLI), jq, ripgrep, fd-find, vim, nano, less, unzip, ssh client |
| **Python 3** | Lazy-installed via mise (see table below) |
| **Homebrew** | Linux-native Homebrew (`/home/linuxbrew/.linuxbrew`) — `brew` on PATH |
| **mise** | Dev tool manager — tools listed below install on first use via `brew` backend |
| **CLI utilities** | git, curl, jq, less, unzip, ssh client |
| **Init** | tini as PID 1 (zombie reaping, clean shutdown) |

### Lazy-installed tools

These tools install on first use (via mise → Homebrew):

| Tool | Command | Backend |
|---|---|---|
| GitHub CLI | `gh` | brew |
| GitLab CLI | `glab` | brew |
| Ruby | `ruby` | brew |
| ripgrep | `rg` | brew |
| fd | `fd` | brew |
| Wget | `wget` | brew |
| Vim | `vim` | brew |
| Micro | `micro` | brew |
| Nano | `nano` | brew |
| Python 3 | `python3` | brew |
| Node.js | `node` | brew |

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.

## Usage

### Quick start
Expand Down Expand Up @@ -83,4 +103,5 @@ Fetches the latest release from [anomalyco/opencode](https://github.com/anomalyc
- The root filesystem is ephemeral; mount `/home/opencode` as the persistent volume for all user data (dotfiles, config, projects). The `~/workspace` subdirectory is the default workdir.
- `~/.local/bin` is on PATH and user-writable, useful for dropping custom tools at runtime.
- Node version can be switched at runtime with `n <version>` (e.g. `n lts`).
- Homebrew is installed under `/home/linuxbrew/.linuxbrew` (outside the persistent volume) and is usable immediately by the `opencode` user.
- Homebrew is installed under `/home/linuxbrew/.linuxbrew` (outside the persistent volume). It uses its bundled portable Ruby — no system Ruby needed.
- **Lazy-installed tools** (see table above): run any listed tool and mise auto-installs it via Homebrew on first use. The image ships defaults in `/etc/mise/config.toml`; create `~/.config/mise/config.toml` to add your own — mise merges both.
12 changes: 12 additions & 0 deletions mise-config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[tools]
"brew:gh" = "latest"
"brew:glab" = "latest"
"brew:ruby" = "latest"
"brew:ripgrep" = "latest"
"brew:fd" = "latest"
"brew:wget" = "latest"
"brew:vim" = "latest"
"brew:micro" = "latest"
"brew:nano" = "latest"
"brew:python" = "latest"
"brew:node" = "latest"
Loading