feat(dogfood/coder): add brew and mise to ubuntu images (#24618)

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.

---

<details>
<summary>📋 Implementation Plan</summary>

# 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 <tool>`
   - run `<tool> --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.

<details>
<summary>Why this is the lowest-risk path</summary>

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.
</details>

</details>

---
_Generated with [`mux`](https://github.com/coder/mux) • Model:
`openai:gpt-5.5` • Thinking: `xhigh`_
This commit is contained in:
Thomas Kosiewski
2026-05-07 09:24:08 +02:00
committed by GitHub
parent 2ff05608d2
commit f605d6bcb4
3 changed files with 106 additions and 11 deletions
+39
View File
@@ -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
+34 -6
View File
@@ -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.
+33 -5
View File
@@ -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.