From f605d6bcb4a295d6fbee0314c4608b912f90acf5 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Thu, 7 May 2026 09:24:08 +0200 Subject: [PATCH] feat(dogfood/coder): add brew and mise to ubuntu images (#24618) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds Homebrew and mise to the Ubuntu dogfood images and makes mise shims win PATH resolution for the `coder` user. It installs Homebrew in `/home/linuxbrew/.linuxbrew`, installs the latest mise release (`v2026.4.19`) via its verified GitHub release artifact, exposes mise at `/usr/local/bin/mise`, wires `HOMEBREW_*` and `MISE_DATA_DIR`, and adds build-time checks for both tools. The mise executable target lives in writable `/opt/mise/bin` so `mise self-update` can replace it as the `coder` user. This also adds `libc6-dev` to the Go utility stages so the existing CGO-backed tool installs keep building on newer Ubuntu bases. The dogfood template now mounts a dedicated `/home/linuxbrew/` Docker volume in addition to `/home/coder/`. Fresh volumes are seeded from the image-baked Homebrew tree on first mount, while user-installed formulae persist across workspace container recreation. I revalidated the bootstrap on jammy and resolute base images with fresh mounted `/home/coder` and `/home/linuxbrew` volumes. In those runs, `brew install hello` succeeded, `mise doctor` reported no PATH or activation problems, `mise self-update --force --yes --no-plugins 2026.4.19` succeeded as `coder`, and `mise use --global github:BurntSushi/ripgrep@14.1.1` moved `rg` resolution to the mise shim after container recreation. ---
📋 Implementation Plan # Plan: add `mise` and Homebrew to the dogfood Ubuntu images with mise-first PATH ## Goal - Make both dogfood Ubuntu images ship `brew` and `mise`. - Ensure `mise doctor` does **not** complain about activation/PATH ordering in the shell entrypoints we support. - Keep the implementation robust against the persistent `/home/coder` volume used by the dogfood template. ## Verified context - The relevant image definitions are: - `dogfood/coder/ubuntu-22.04/Dockerfile` - `dogfood/coder/ubuntu-26.04/Dockerfile` - The dogfood template mounts a persistent home volume at `/home/coder/` in `dogfood/coder/main.tf:840-843`, so required image-baked state should not live only under `/home/coder`. - Both Dockerfiles already manipulate PATH in multiple places: - Go appended early (`:26`) - Cargo prepended (`ubuntu-26.04/Dockerfile:202-206`; mirrored in 22.04) - Node via nvm prepended (`ubuntu-26.04/Dockerfile:245-255`; mirrored in 22.04) - Final `coder` PATH prepends `/home/coder/go/bin` (`ubuntu-26.04/Dockerfile:348-358`; mirrored in 22.04) - `COPY files /` is already present in both Dockerfiles, so adding new global shell-init files is possible without Terraform changes. - `scripts/lib.sh:94-124` uses `command -v` for dependency detection, so PATH order is the practical repo-level behavior we need to control. - `.github/workflows/dogfood.yaml:99-126` builds both Ubuntu variants, and the 22.04 image is still tagged `latest`, so both Dockerfiles must be updated in the same change. ## Recommended implementation ### Phase 1 — Bootstrap Homebrew and `mise` in both Ubuntu Dockerfiles 1. Update both Dockerfiles in parallel: - `dogfood/coder/ubuntu-22.04/Dockerfile` - `dogfood/coder/ubuntu-26.04/Dockerfile` 2. Add the minimum explicit Homebrew prerequisites that are missing from the current apt package set. - The images already install `build-essential`, `curl`, `file`, and `git`. - Audit whether `procps` must be added explicitly for Homebrew’s Linux requirements. 3. Install Homebrew in the supported Linux prefix: - Prefix: `/home/linuxbrew/.linuxbrew` - Keep the install/build logic in the Dockerfile, before `USER coder`. - Make the resulting prefix writable by `coder` before switching users. Prefer the smallest-diff approach that leaves `brew install ...` usable as `coder`. 4. Install `mise` to a stable image-owned path instead of relying on `~/.local/bin`: - Preferred binary path: `/usr/local/bin/mise` - Use a pinned installation method that fits the current Dockerfile style (versioned release asset or otherwise explicitly pinned installer path). 5. Add defensive build-time sanity checks near the install steps so the image fails early if assumptions are wrong: - `test -x /usr/local/bin/mise` - `test -x /home/linuxbrew/.linuxbrew/bin/brew` - `brew --version` - `mise --version` **Quality gate:** both Dockerfiles build locally, and the resulting container can run `brew --version` and `mise --version` as `coder`. ### Phase 2 — Make `mise` win PATH resolution by default 1. After `USER coder` in both Dockerfiles, define stable environment variables for the final shell/runtime behavior: - `HOMEBREW_PREFIX=/home/linuxbrew/.linuxbrew` - `MISE_DATA_DIR=/home/coder/.local/share/mise` - `MISE_ACTIVATE_AGGRESSIVE=1` only if later shell activation proves necessary 2. Replace the final PATH composition so it resolves in this order: 1. `mise` shims 2. Homebrew `bin`/`sbin` 3. Existing `/home/coder/go/bin` 4. Existing image/system PATH 3. Keep the current Go/Rust/Node setup intact aside from the final PATH ordering. Add a short Dockerfile comment explaining that `mise` shims must be first so `mise doctor` and `command -v` resolve `mise`-managed tools ahead of Homebrew/system binaries. 4. Do **not** rely on image-baked `mise` state under `/home/coder` for the initial implementation. The goal here is binary availability and path precedence, not preinstalling shared `mise` toolchains. **Quality gate:** in a fresh container as `coder`, `echo "$PATH"` shows `mise` shims before Homebrew, and `mise doctor`/`mise doctor path` show no PATH or activation problem in the tested shell entrypoints. ### Phase 3 — Add shell-init hardening only if smoke tests prove it is needed 1. Start with the Dockerfile `ENV PATH` solution as the default behavior. 2. If dogfooding shows that supported login shells still need shell integration beyond the final `ENV PATH`, add minimal global shell-init files under: - `dogfood/coder/ubuntu-22.04/files/etc/profile.d/` - `dogfood/coder/ubuntu-26.04/files/etc/profile.d/` 3. If these files are needed, keep them narrowly scoped: - a Homebrew file that exports/evals `brew shellenv` - a `mise` file that only reinforces the intended shims-first behavior 4. Avoid touching per-user dotfiles in `/home/coder`; they are the wrong place for required image behavior because of the persistent home volume. **Quality gate:** if profile.d files are added, login-shell smoke tests pass and we do not introduce new PATH-order regressions versus the Dockerfile-only path. ## Acceptance criteria - Both Ubuntu dogfood Dockerfiles are updated in one change and still build. - `brew` is installed in `/home/linuxbrew/.linuxbrew` and is usable as `coder`. - `mise` is installed at `/usr/local/bin/mise` and is usable as `coder`. - `mise doctor` does not report an activation/PATH-ordering problem in the shell entrypoints we verify. - Final PATH precedence is: 1. `mise` shims 2. Homebrew `bin`/`sbin` 3. existing user/tool paths 4. system paths - Existing dogfood workflows still work for Go/Rust/Node tooling after the PATH change. - The change passes the dogfood image CI path in `.github/workflows/dogfood.yaml`. ## Dogfooding and verification 1. Build both images locally: - `dogfood/coder/ubuntu-22.04` - `dogfood/coder/ubuntu-26.04` 2. Run each image with an empty mounted home volume at `/home/coder` to mimic the actual dogfood runtime constraint instead of only testing the image’s baked filesystem. 3. Capture a short terminal recording and screenshots for each variant showing: - `brew --prefix` - `brew --version` - `mise --version` - `echo "$PATH"` - `mise doctor` - `mise doctor path` 4. Verify at least one login-shell path and one non-login-shell path, so we can tell whether Dockerfile `ENV PATH` is sufficient or whether `/etc/profile.d` hardening is required. 5. Add one tool-resolution smoke test that proves `mise` wins when configured: - install/use a small `mise`-managed runtime as `coder` - run `which -a ` - run ` --version` 6. Verify existing image behavior did not regress: - `go version` - `node --version` - any other must-have image tools that were already on PATH 7. Preserve the artifacts from dogfooding for review: - screenshots attached to the change summary - a short screen recording (or terminal recording) covering the smoke test ## Risks and decision points - **Homebrew ownership model:** installing Homebrew during `docker build` is not enough by itself; the prefix must end up writable for `coder`. - **Scope control:** the initial change should solve `mise doctor` by fixing PATH precedence, not by introducing a larger `mise`-managed tool bootstrap. - **Shell-init uncertainty:** if the dogfood terminal entrypoints do not source `/etc/profile`, a Dockerfile `ENV PATH` fix may be sufficient and profile.d may be unnecessary. This should be decided by smoke tests, not by assumption. - **Persistent home behavior:** avoid any required implementation detail that only works if fresh volumes copy image-baked `/home/coder` contents.
Why this is the lowest-risk path This plan keeps the initial implementation focused on the user’s stated goal: install Homebrew and `mise`, then guarantee that `mise`-controlled paths win so `mise doctor` stays quiet. The main repo-specific constraint is the persistent `/home/coder` volume. That pushes required binaries and ownership-sensitive state out of `/home/coder` where possible, and it argues against relying on user dotfiles for required image behavior. Starting with Dockerfile-level install steps plus a final PATH reorder keeps the diff small, makes behavior consistent across shells, and gives us a clean place to add shell-init hardening only if the smoke tests prove it is necessary.
--- _Generated with [`mux`](https://github.com/coder/mux) • Model: `openai:gpt-5.5` • Thinking: `xhigh`_ --- dogfood/coder/main.tf | 39 ++++++++++++++++++++++++++ dogfood/coder/ubuntu-22.04/Dockerfile | 40 +++++++++++++++++++++++---- dogfood/coder/ubuntu-26.04/Dockerfile | 38 +++++++++++++++++++++---- 3 files changed, 106 insertions(+), 11 deletions(-) diff --git a/dogfood/coder/main.tf b/dogfood/coder/main.tf index 70f7a8b2e4..869be938a8 100644 --- a/dogfood/coder/main.tf +++ b/dogfood/coder/main.tf @@ -753,6 +753,38 @@ resource "docker_volume" "home_volume" { } } +resource "coder_metadata" "homebrew_volume" { + resource_id = docker_volume.homebrew_volume.id + hide = true # Hide it as it only backs Homebrew state. +} + +resource "docker_volume" "homebrew_volume" { + name = "coder-${data.coder_workspace.me.id}-homebrew" + # Protect the volume from being deleted due to changes in attributes. + lifecycle { + ignore_changes = all + } + # Add labels in Docker to keep track of orphan resources. + labels { + label = "coder.owner" + value = data.coder_workspace_owner.me.name + } + labels { + label = "coder.owner_id" + value = data.coder_workspace_owner.me.id + } + labels { + label = "coder.workspace_id" + value = data.coder_workspace.me.id + } + # This field becomes outdated if the workspace is renamed but can + # be useful for debugging or cleaning out dangling volumes. + labels { + label = "coder.workspace_name_at_creation" + value = data.coder_workspace.me.name + } +} + resource "coder_metadata" "docker_volume" { resource_id = docker_volume.docker_volume.id hide = true # Hide it as it is not useful to see in the UI. @@ -850,6 +882,13 @@ resource "docker_container" "workspace" { volume_name = docker_volume.home_volume.name read_only = false } + # Homebrew is baked into this path. A Docker named volume copies the + # image contents on first mount, then persists user-installed formulae. + volumes { + container_path = "/home/linuxbrew/" + volume_name = docker_volume.homebrew_volume.name + read_only = false + } volumes { container_path = "/var/lib/docker/" volume_name = docker_volume.docker_volume.name diff --git a/dogfood/coder/ubuntu-22.04/Dockerfile b/dogfood/coder/ubuntu-22.04/Dockerfile index 62ab6c1334..d984a5bd37 100644 --- a/dogfood/coder/ubuntu-22.04/Dockerfile +++ b/dogfood/coder/ubuntu-22.04/Dockerfile @@ -27,7 +27,7 @@ ENV PATH=$PATH:/usr/local/go/bin ARG GOPATH="/tmp/" # Install Go utilities. RUN apt-get update && \ - apt-get install --yes gcc && \ + apt-get install --yes gcc libc6-dev && \ mkdir --parents /usr/local/go && \ tar --extract --gzip --directory=/usr/local/go --file=/usr/local/go.tar.gz --strip-components=1 && \ mkdir --parents "$GOPATH" && \ @@ -163,6 +163,7 @@ RUN sed -i 's|http://archive.ubuntu.com/ubuntu/|http://mirrors.edge.kernel.org/u openssh-server \ openssl \ pkg-config \ + procps \ postgresql-16 \ python3 \ python3-pip \ @@ -288,7 +289,10 @@ ARG GOLANGCI_LINT_VERSION=1.64.8 \ KUBECTX_VERSION=0.9.4 \ SYFT_VERSION=1.20.0 \ COSIGN_VERSION=2.4.3 \ - BUN_VERSION=1.2.15 + BUN_VERSION=1.2.15 \ + MISE_VERSION=v2026.4.19 \ + MISE_SHA256=6b58ff5f1e1ce98ed2b7e5372c344ea48182c460e5b6df12d9e0def35aad4438 \ + MISE_INSTALL_DIR=/opt/mise/bin RUN \ # golangci-lint performs static code analysis for our Go code @@ -324,6 +328,25 @@ RUN useradd coder \ --uid=1000 \ --user-group +# Install mise to a stable path outside /home/coder, but keep its target +# directory writable so `mise self-update` can replace the binary as coder. +RUN install --directory --owner=coder --group=coder --mode=0755 "${MISE_INSTALL_DIR}" && \ + curl --silent --show-error --location --fail \ + "https://github.com/jdx/mise/releases/download/${MISE_VERSION}/mise-${MISE_VERSION}-linux-x64" \ + --output "${MISE_INSTALL_DIR}/mise" && \ + echo "${MISE_SHA256} ${MISE_INSTALL_DIR}/mise" | sha256sum -c && \ + chown coder:coder "${MISE_INSTALL_DIR}/mise" && \ + chmod 0755 "${MISE_INSTALL_DIR}/mise" && \ + ln --symbolic "${MISE_INSTALL_DIR}/mise" /usr/local/bin/mise && \ + test -x /usr/local/bin/mise && \ + sudo --login --user=coder /bin/bash -lc 'set -euo pipefail && mise_bin="$(readlink --canonicalize /usr/local/bin/mise)" && test -w "$(dirname "$mise_bin")" && /usr/local/bin/mise --version && /usr/local/bin/mise self-update --help >/dev/null && /usr/local/bin/mise upgrade --help >/dev/null' + +# Install Homebrew as the coder user so the supported Linux prefix remains +# writable after the image build. +RUN sudo --login --user=coder env NONINTERACTIVE=1 CI=1 /bin/bash -lc 'set -euo pipefail && curl --silent --show-error --location --fail https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh | /bin/bash' && \ + test -x /home/linuxbrew/.linuxbrew/bin/brew && \ + sudo --login --user=coder /bin/bash -lc '/home/linuxbrew/.linuxbrew/bin/brew --version' + # Adjust OpenSSH config RUN echo "PermitUserEnvironment yes" >>/etc/ssh/sshd_config && \ echo "X11Forwarding yes" >>/etc/ssh/sshd_config && \ @@ -346,10 +369,15 @@ COPY --from=proto /tmp/include /usr/local/bin/include USER coder -# Ensure go bins are in the 'coder' user's path. Note that no go bins are -# installed in this docker file, as they'd be mounted over by the persistent -# home volume. -ENV PATH="/home/coder/go/bin:${PATH}" +# Configure Homebrew and mise for the coder user. mise shims must stay first +# so `command -v` and `mise doctor` resolve mise-managed tools ahead of +# Homebrew and system binaries. Note that no go bins are installed in this +# docker file, as they'd be mounted over by the persistent home volume. +ENV HOMEBREW_PREFIX="/home/linuxbrew/.linuxbrew" \ + HOMEBREW_CELLAR="/home/linuxbrew/.linuxbrew/Cellar" \ + HOMEBREW_REPOSITORY="/home/linuxbrew/.linuxbrew/Homebrew" \ + MISE_DATA_DIR="/home/coder/.local/share/mise" +ENV PATH="${MISE_DATA_DIR}/shims:${HOMEBREW_PREFIX}/bin:${HOMEBREW_PREFIX}/sbin:/home/coder/go/bin:${PATH}" # Override CARGO_HOME so cargo registry/cache writes go to the coder # user's home directory instead of the root-owned /usr/local/cargo. diff --git a/dogfood/coder/ubuntu-26.04/Dockerfile b/dogfood/coder/ubuntu-26.04/Dockerfile index 82de9a7a02..e0f7ee2fca 100644 --- a/dogfood/coder/ubuntu-26.04/Dockerfile +++ b/dogfood/coder/ubuntu-26.04/Dockerfile @@ -162,6 +162,7 @@ RUN sed -i 's|http://archive.ubuntu.com/ubuntu/|http://mirrors.edge.kernel.org/u openssh-server \ openssl \ pkg-config \ + procps \ postgresql-18 \ python3 \ python3-pip \ @@ -286,7 +287,10 @@ ARG GOLANGCI_LINT_VERSION=1.64.8 \ KUBECTX_VERSION=0.9.4 \ SYFT_VERSION=1.20.0 \ COSIGN_VERSION=2.4.3 \ - BUN_VERSION=1.2.15 + BUN_VERSION=1.2.15 \ + MISE_VERSION=v2026.4.19 \ + MISE_SHA256=6b58ff5f1e1ce98ed2b7e5372c344ea48182c460e5b6df12d9e0def35aad4438 \ + MISE_INSTALL_DIR=/opt/mise/bin RUN \ # golangci-lint performs static code analysis for our Go code @@ -325,6 +329,25 @@ RUN userdel -r ubuntu && \ --uid=1000 \ --user-group +# Install mise to a stable path outside /home/coder, but keep its target +# directory writable so `mise self-update` can replace the binary as coder. +RUN install --directory --owner=coder --group=coder --mode=0755 "${MISE_INSTALL_DIR}" && \ + curl --silent --show-error --location --fail \ + "https://github.com/jdx/mise/releases/download/${MISE_VERSION}/mise-${MISE_VERSION}-linux-x64" \ + --output "${MISE_INSTALL_DIR}/mise" && \ + echo "${MISE_SHA256} ${MISE_INSTALL_DIR}/mise" | sha256sum -c && \ + chown coder:coder "${MISE_INSTALL_DIR}/mise" && \ + chmod 0755 "${MISE_INSTALL_DIR}/mise" && \ + ln --symbolic "${MISE_INSTALL_DIR}/mise" /usr/local/bin/mise && \ + test -x /usr/local/bin/mise && \ + sudo --login --user=coder /bin/bash -lc 'set -euo pipefail && mise_bin="$(readlink --canonicalize /usr/local/bin/mise)" && test -w "$(dirname "$mise_bin")" && /usr/local/bin/mise --version && /usr/local/bin/mise self-update --help >/dev/null && /usr/local/bin/mise upgrade --help >/dev/null' + +# Install Homebrew as the coder user so the supported Linux prefix remains +# writable after the image build. +RUN sudo --login --user=coder env NONINTERACTIVE=1 CI=1 /bin/bash -lc 'set -euo pipefail && curl --silent --show-error --location --fail https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh | /bin/bash' && \ + test -x /home/linuxbrew/.linuxbrew/bin/brew && \ + sudo --login --user=coder /bin/bash -lc '/home/linuxbrew/.linuxbrew/bin/brew --version' + # Adjust OpenSSH config RUN echo "PermitUserEnvironment yes" >>/etc/ssh/sshd_config && \ echo "X11Forwarding yes" >>/etc/ssh/sshd_config && \ @@ -347,10 +370,15 @@ COPY --from=proto /tmp/include /usr/local/bin/include USER coder -# Ensure go bins are in the 'coder' user's path. Note that no go bins are -# installed in this docker file, as they'd be mounted over by the persistent -# home volume. -ENV PATH="/home/coder/go/bin:${PATH}" +# Configure Homebrew and mise for the coder user. mise shims must stay first +# so `command -v` and `mise doctor` resolve mise-managed tools ahead of +# Homebrew and system binaries. Note that no go bins are installed in this +# docker file, as they'd be mounted over by the persistent home volume. +ENV HOMEBREW_PREFIX="/home/linuxbrew/.linuxbrew" \ + HOMEBREW_CELLAR="/home/linuxbrew/.linuxbrew/Cellar" \ + HOMEBREW_REPOSITORY="/home/linuxbrew/.linuxbrew/Homebrew" \ + MISE_DATA_DIR="/home/coder/.local/share/mise" +ENV PATH="${MISE_DATA_DIR}/shims:${HOMEBREW_PREFIX}/bin:${HOMEBREW_PREFIX}/sbin:/home/coder/go/bin:${PATH}" # Override CARGO_HOME so cargo registry/cache writes go to the coder # user's home directory instead of the root-owned /usr/local/cargo.