Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
129 changes: 98 additions & 31 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,42 +1,109 @@
FROM mcr.microsoft.com/devcontainers/typescript-node:24-bookworm
# syntax=docker/dockerfile:1
#
# Dockerfile-only devcontainer: no devcontainer `features:`.
#
# Coder builds this workspace with envbuilder, and envbuilder's prebuilt-image
# cache probe (the `envbuilder_cached_image` Terraform resource) cannot reproduce
# devcontainer *feature* layers, so a feature-based image always misses the cache
# and rebuilds on every workspace start (coder/terraform-provider-envbuilder#68).
# Everything is therefore installed from plain RUN layers here. Tool versions
# live in mise.toml (installed system-wide with mise); only the few things mise
# cannot manage (apt packages, nsc, Playwright) are installed directly.
FROM mcr.microsoft.com/devcontainers/typescript-node:26-trixie

# Wire up the tmux configuration for the default container user (node)
COPY tmux.conf /home/node/.tmux.conf
RUN chown node:node /home/node/.tmux.conf
USER root
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
ARG DEBIAN_FRONTEND=noninteractive

# ---------------------------------------------------------------------------
# APT packages: general toolbox, sshd, and the headers native Ruby gems and
# ruby-build need (libyaml/openssl/readline/ffi/gdbm/zlib).
# tmux is intentionally NOT installed from apt; a current release comes from mise.
# ---------------------------------------------------------------------------
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential git postgresql-client curl htop vim \
ca-certificates unzip \
openssh-server \
autoconf bison libssl-dev libyaml-dev libreadline-dev zlib1g-dev \
libncurses-dev libffi-dev libgdbm-dev libdb-dev uuid-dev \
&& rm -rf /var/lib/apt/lists/*

# ---------------------------------------------------------------------------
# sshd (replaces ghcr.io/devcontainers/features/sshd). Listens on 2222 and is
# started by postStartCommand in devcontainer.json.
# ---------------------------------------------------------------------------
RUN mkdir -p /var/run/sshd \
&& ssh-keygen -A \
&& sed -i 's/^#\?Port .*/Port 2222/' /etc/ssh/sshd_config \
&& sed -i 's/^#\?PasswordAuthentication .*/PasswordAuthentication yes/' /etc/ssh/sshd_config

# Also wire up tmux configuration for root (used when connecting via `coder ssh`)
# ---------------------------------------------------------------------------
# /workspaces is created root-owned by the base image and envbuilder does not
# chown it, so `git clone` into it failed for the node user. Hand it over.
# ---------------------------------------------------------------------------
RUN mkdir -p /workspaces && chown node:node /workspaces

# ---------------------------------------------------------------------------
# tmux configuration for both `node` (default) and `root` (used by `coder ssh`).
# ---------------------------------------------------------------------------
COPY tmux.conf /home/node/.tmux.conf
COPY tmux.conf /root/.tmux.conf
RUN chown root:root /root/.tmux.conf
RUN chown node:node /home/node/.tmux.conf && chown root:root /root/.tmux.conf

# Install the Namespace (nsc) CLI - https://namespace.so/docs/reference/cli/installation
# Install system-wide so it is on PATH for both the `node` and `root` users.
# ---------------------------------------------------------------------------
# Namespace (nsc) CLI - https://namespace.so/docs/reference/cli/installation
# Installed system-wide so it is on PATH for both `node` and `root`.
# ---------------------------------------------------------------------------
RUN curl -fsSL https://get.namespace.so/cloud/install.sh | NS_INSTALL_DIR=/usr/local/bin sh

# Install Playwright with the Chromium browser and its required OS dependencies.
# Browsers are stored in a shared, system-wide location (PLAYWRIGHT_BROWSERS_PATH)
# so both the `node` and `root` users can run Playwright without re-downloading.
# ---------------------------------------------------------------------------
# Playwright with Chromium and its OS dependencies. Browsers live in a shared,
# system-wide location so both users can run Playwright without re-downloading.
# ---------------------------------------------------------------------------
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
RUN npm install -g playwright \
&& playwright install --with-deps chromium \
&& chmod -R a+rx /ms-playwright

# Tell dev container features where the target user's home directory is.
# The devcontainer CLI passes `_REMOTE_USER_HOME`/`_CONTAINER_USER_HOME` to every
# feature's install.sh, but envbuilder (used by Coder) only sets `_REMOTE_USER`
# and `_CONTAINER_USER` -- it leaves the *_HOME variables empty. Features that
# rely on them then break: e.g. the claude-code feature runs
# `cp "$_REMOTE_USER_HOME/.local/bin/claude" /usr/local/bin/claude`, which under
# envbuilder expands to `cp /.local/bin/claude ...` and fails the whole build.
# Supplying the values here (as build ARGs, so they don't leak into the running
# container's environment) keeps feature installs working on the envbuilder path.
# ARGs declared here remain in scope for the feature install steps envbuilder
# appends after this Dockerfile, but reset for the devcontainer CLI (which sets
# these variables itself), so this is a no-op there.
ARG _REMOTE_USER_HOME=/home/node
ARG _CONTAINER_USER_HOME=/home/node

# Default to the non-root `node` user. envbuilder (used by Coder) picks its target
&& chmod -R a+rx /ms-playwright \
&& rm -rf /var/lib/apt/lists/*

# ---------------------------------------------------------------------------
# mise (https://mise.jdx.dev) manages every versioned tool: languages, k8s
# CLIs, gh/az, coding agents, tmux. Pinned binary copied from the official
# image; versions pinned in mise.toml (-> /etc/mise/config.toml).
#
# Layout:
# - MISE_DATA_DIR=/usr/local/share/mise : installs + shims, shared by all
# users and outside $HOME so a mounted/persisted home cannot shadow it.
# - shims dir on PATH via ENV, so tools work in non-interactive shells
# (VS Code server, Coder agent, CI `devcontainer exec`) without `mise
# activate`. Interactive shells may still `eval "$(mise activate bash)"`.
# - GOPATH / CARGO_HOME / RUSTUP_HOME are system-wide and node-owned so
# `go install` / `cargo install` work and persist in the image.
# ---------------------------------------------------------------------------
COPY --from=ghcr.io/jdx/mise:2026.10.23 /usr/local/bin/mise /usr/local/bin/mise
ENV MISE_DATA_DIR=/usr/local/share/mise \
MISE_TRUSTED_CONFIG_PATHS=/etc/mise \
GOPATH=/go \
RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo
ENV PATH=${MISE_DATA_DIR}/shims:${GOPATH}/bin:${CARGO_HOME}/bin:${PATH}
COPY mise.toml /etc/mise/config.toml
RUN mkdir -p "${MISE_DATA_DIR}" "${GOPATH}" "${RUSTUP_HOME}" "${CARGO_HOME}" \
&& chown -R node:node "${MISE_DATA_DIR}" "${GOPATH}" "${RUSTUP_HOME}" "${CARGO_HOME}"

# Install as `node` so the shared tool tree is owned by the user who will add
# to it (gems, `go install`, `cargo install`, `mise use`). root can still run
# and modify everything.
USER node
RUN MISE_YES=1 mise install --jobs 8 \
&& mise reshim \
&& missing="$(mise ls --missing)" \
&& if [ -n "$missing" ]; then echo "mise tools missing after install:" "$missing" >&2; exit 1; fi \
&& rm -rf /home/node/.cache/mise

# Stay on the non-root `node` user. envbuilder (used by Coder) picks its target
# user from the last `USER` directive when `containerUser`/`remoteUser` are not
# applied from image metadata, so make the intended user explicit here to avoid
# dropping into a root shell.
# applied from image metadata, so the intended user must be explicit here to
# avoid dropping into a root shell.
USER node
64 changes: 0 additions & 64 deletions .devcontainer/devcontainer-lock.json

This file was deleted.

61 changes: 17 additions & 44 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,35 +1,14 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
// For format details, see https://aka.ms/devcontainer.json.
//
// This devcontainer deliberately uses NO `features:`. Everything is installed by
// the Dockerfile (tool versions in mise.toml) so that Coder's envbuilder cache
// probe can reproduce the build and boot workspaces straight from the prebuilt
// image. See the Dockerfile header and README for details.
{
"name": "bmorton/devcontainer",
"build": {
"dockerfile": "Dockerfile"
},
"features": {
"ghcr.io/devcontainers-extra/features/apt-packages:1": {
"packages": "build-essential,git,postgresql-client,curl,htop,tmux,vim"
},
"ghcr.io/devcontainers/features/go:1": {
"version": "1.26.3"
},
"ghcr.io/devcontainers-extra/features/ruby-asdf:0": {
"version": "4.0.5"
},
"ghcr.io/devcontainers/features/rust:1": {},
"ghcr.io/devcontainers/features/sshd:1": {},
"ghcr.io/devcontainers/features/kubectl-helm-minikube:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/azure-cli:1": {
"version": "latest"
},
"ghcr.io/devcontainers/features/copilot-cli:1": {},
"ghcr.io/devcontainers-extra/features/claude-code:2": {},
"ghcr.io/devcontainers-extra/features/opencode:1": {},
"ghcr.io/devcontainers-extra/features/npm-package:1": {
"package": "@openprose/prose-cli"
}
},
"overrideFeatureInstallOrder": ["ghcr.io/devcontainers/features/rust"],

// Connect as the `node` user. Set this explicitly rather than relying on the
// base image's `remoteUser` metadata: envbuilder (used by Coder) does not honor
Expand All @@ -38,16 +17,12 @@
"remoteUser": "node",
"containerUser": "node",

// Don't remap the container user's UID/GID to match the host user. The sshd
// feature creates an `ssh` group at GID 1001, which collides with the host
// UID/GID (1001) used by CI runners. The CLI's automatic remap then fails with
// Don't remap the container user's UID/GID to match the host user. CI runners
// use UID/GID 1001 and the automatic remap has previously failed with
// `groupmod: GID '1001' already exists`, breaking `devcontainer up`.
// (envbuilder ignores this key; it only affects the devcontainer CLI.)
"updateRemoteUserUID": false,

// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],

Expand All @@ -56,13 +31,19 @@
// completion (both the devcontainer CLI and envbuilder honor this hook).
"postCreateCommand": "kubectl version --client 2>/dev/null || true; touch \"$HOME/.devcontainer-postcreate-done\"",

// Start sshd (port 2222) on every container start; this replaces the entrypoint
// the sshd devcontainer feature used to install. Never fails the start.
"postStartCommand": "sudo /usr/sbin/sshd 2>/dev/null || true",

// Configure tool-specific properties.
"customizations": {
"vscode": {
"extensions": [
// General development
"eamodio.gitlens",
"sourcegraph.cody-ai",
"github.vscode-pull-request-github",
"github.vscode-github-actions",
"redhat.vscode-yaml",

// Ruby extensions
"shopify.ruby-lsp",
Expand All @@ -75,9 +56,7 @@
"ms-kubernetes-tools.vscode-kubernetes-tools",
],
"settings": {
// Ruby settings
"ruby.useBundler": true,
"ruby.format": "rubocop",
// Ruby settings (Ruby LSP; the old rebornix `ruby.*` keys were removed)
"rubyLsp.formatter": "rubocop",
"rubyLsp.enabledFeatures": {
"documentHighlights": true,
Expand All @@ -89,9 +68,6 @@
"codeActions": true,
"diagnostics": true
},
"ruby.lint": {
"rubocop": true
},

// Go settings
"go.useLanguageServer": true,
Expand All @@ -111,8 +87,5 @@
"files.insertFinalNewline": true
}
}
},

// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}
}
61 changes: 61 additions & 0 deletions .devcontainer/mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# System-wide tool versions for the devcontainer.
#
# This file is baked into the image as /etc/mise/config.toml and installed
# during the Docker build with `mise install`, so every tool below is present
# for both the `node` and `root` users, on PATH via mise shims, with no shell
# activation required.
#
# Versions are pinned exactly so the image (and the envbuilder cache built from
# it) is reproducible. The weekly "Bump mise tools" workflow runs
# `mise upgrade --bump` and opens a PR when newer versions are available.
#
# Projects opened inside the container can add their own mise.toml to override
# any of these (e.g. a different Go or Ruby) -- project config layers on top of
# this system config.

[settings]
# Run pipx-backed tools (azure-cli) through uv instead of pipx.
pipx.uvx = true

[tools]
# --- Languages ---------------------------------------------------------------
go = "1.27.3"
ruby = "4.0.9"
rust = "1.99.1"
uv = "0.13.6"

# --- Go tooling (previously installed by the Go devcontainer feature) --------
golangci-lint = "2.15.0"
staticcheck = "2026.2.2"
"go:golang.org/x/tools/gopls" = "0.24.0"
"go:github.com/go-delve/delve/cmd/dlv" = "1.27.2"
"go:golang.org/x/vuln/cmd/govulncheck" = "1.9.0"

# --- Kubernetes --------------------------------------------------------------
kubectl = "1.37.2"
helm = "4.3.1"
minikube = "1.39.0"

# --- Cloud / platform CLIs ---------------------------------------------------
github-cli = "2.103.3"
"pipx:azure-cli" = "2.91.0"

# --- Coding agents -----------------------------------------------------------
# Claude Code self-updates into ~/.local/bin, which is ahead of the mise shims on
# PATH, so this pin is the floor version in a fresh container, not a ceiling.
# (mise ignores releases younger than 24h by default, so `mise upgrade --bump`
# may trail the very latest release by a day.)
claude = "2.1.312"
copilot-cli = "1.0.105"
opencode = "2.2.1"
pi = "1.4.0"

# --- Utilities ---------------------------------------------------------------
# witr: "why is this running?" process explainer (was proposed in PR #21).
witr = "0.3.4"

# --- Terminal ----------------------------------------------------------------
# Official static builds from github.com/tmux/tmux-builds; far newer than the
# Debian package and required for allow-passthrough/extended-keys behaviour
# that Claude Code and iTerm2 rely on.
tmux = "3.7c"
Loading