mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user