Skip to content
Draft
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
157 changes: 128 additions & 29 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,42 +1,141 @@
# syntax=docker/dockerfile:1
#
# Dockerfile-only devcontainer (no devcontainer `features:`).
#
# Why: Coder builds workspaces with envbuilder, and envbuilder's prebuilt-image
# cache probe (the `envbuilder_cached_image` Terraform resource) cannot reproduce
# devcontainer *feature* layers -- features install from `/.envbuilder/features/...`
# during the build but from `/tmp/...` during the probe, so the probe always misses
# and the workspace rebuilds. See coder/terraform-provider-envbuilder#68.
#
# Folding every tool into plain Dockerfile RUN layers lets the probe reproduce the
# build byte-for-byte, so a cached workspace can boot directly from the prebuilt
# image (no build, no layer extraction). The toolset mirrors the previous
# feature-based devcontainer.json.
FROM mcr.microsoft.com/devcontainers/typescript-node:24-bookworm

# 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
# All install steps run as root; the final USER directive drops back to `node`.
USER root
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
ENV DEBIAN_FRONTEND=noninteractive

# ---------------------------------------------------------------------------
# APT packages: the requested toolbox plus Ruby build dependencies and sshd.
# ---------------------------------------------------------------------------
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential git postgresql-client curl htop tmux vim \
ca-certificates gnupg lsb-release wget 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/*

# Also wire up tmux configuration for root (used when connecting via `coder ssh`)
# ---------------------------------------------------------------------------
# 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

# ---------------------------------------------------------------------------
# Go (was ghcr.io/devcontainers/features/go, version 1.26.3). System-wide.
# ---------------------------------------------------------------------------
ENV GO_VERSION=1.26.3
ENV GOROOT=/usr/local/go GOPATH=/go
ENV PATH=${GOROOT}/bin:${GOPATH}/bin:${PATH}
RUN curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" -o /tmp/go.tgz \
&& tar -C /usr/local -xzf /tmp/go.tgz \
&& rm /tmp/go.tgz \
&& mkdir -p "${GOPATH}/bin" "${GOPATH}/src" "${GOPATH}/pkg" \
&& chmod -R a+rwX "${GOPATH}"

# ---------------------------------------------------------------------------
# Rust (was ghcr.io/devcontainers/features/rust). System-wide via rustup.
# ---------------------------------------------------------------------------
ENV RUSTUP_HOME=/usr/local/rustup CARGO_HOME=/usr/local/cargo
ENV PATH=${CARGO_HOME}/bin:${PATH}
RUN curl --proto '=https' --tlsv1.2 -fsSL https://sh.rustup.rs \
| sh -s -- -y --no-modify-path --profile minimal --default-toolchain stable \
&& rustup component add clippy rustfmt \
&& chmod -R a+rwX "${RUSTUP_HOME}" "${CARGO_HOME}"

# ---------------------------------------------------------------------------
# Ruby (was ghcr.io/devcontainers-extra/features/ruby-asdf, version 4.0.5).
# Built with ruby-build and installed system-wide to /usr/local/ruby.
# ---------------------------------------------------------------------------
ENV RUBY_VERSION=4.0.5
ENV PATH=/usr/local/ruby/bin:${PATH}
RUN git clone --depth 1 https://github.com/rbenv/ruby-build.git /tmp/ruby-build \
&& PREFIX=/usr/local /tmp/ruby-build/install.sh \
&& ruby-build "${RUBY_VERSION}" /usr/local/ruby \
&& rm -rf /tmp/ruby-build \
&& /usr/local/ruby/bin/gem install bundler --no-document \
&& chmod -R a+rwX /usr/local/ruby/lib/ruby/gems

# 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.
# ---------------------------------------------------------------------------
# kubectl, helm, minikube (was ghcr.io/devcontainers/features/kubectl-helm-minikube).
# ---------------------------------------------------------------------------
RUN KUBECTL_VERSION="$(curl -fsSL https://dl.k8s.io/release/stable.txt)" \
&& curl -fsSL "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" -o /usr/local/bin/kubectl \
&& chmod 0755 /usr/local/bin/kubectl \
&& curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash \
&& curl -fsSL https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 -o /usr/local/bin/minikube \
&& chmod 0755 /usr/local/bin/minikube

# ---------------------------------------------------------------------------
# GitHub CLI (was ghcr.io/devcontainers/features/github-cli).
# ---------------------------------------------------------------------------
RUN mkdir -p -m 755 /etc/apt/keyrings \
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg -o /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/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/*

# ---------------------------------------------------------------------------
# Azure CLI (was ghcr.io/devcontainers/features/azure-cli, latest).
# ---------------------------------------------------------------------------
RUN curl -sL https://aka.ms/InstallAzureCLIDeb | bash \
&& rm -rf /var/lib/apt/lists/*

# ---------------------------------------------------------------------------
# sshd (was ghcr.io/devcontainers/features/sshd). Listens on 2222.
# ---------------------------------------------------------------------------
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

# ---------------------------------------------------------------------------
# Namespace (nsc) CLI - https://namespace.so/docs/reference/cli/installation
# ---------------------------------------------------------------------------
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 + Chromium in a shared, system-wide browser location.
# ---------------------------------------------------------------------------
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
# 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.
# ---------------------------------------------------------------------------
# Global npm CLIs (replacing the claude-code, opencode, copilot-cli and
# npm-package features). Installed system-wide via the base image's Node.
# - @anthropic-ai/claude-code -> `claude`
# - @github/copilot -> `copilot`
# - opencode-ai -> `opencode`
# - @openprose/prose-cli -> prose CLI
# ---------------------------------------------------------------------------
RUN npm install -g \
@anthropic-ai/claude-code \
@github/copilot \
opencode-ai \
@openprose/prose-cli

# Default to 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.
USER node
64 changes: 0 additions & 64 deletions .devcontainer/devcontainer-lock.json

This file was deleted.

47 changes: 15 additions & 32 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,14 @@
"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"],

// This devcontainer intentionally installs its whole toolchain in the
// Dockerfile instead of using devcontainer `features:`. Coder's envbuilder
// prebuilt-image cache probe cannot reproduce feature layers (they install
// from /.envbuilder/features/... during the build but /tmp/... during the
// probe), so a feature-based devcontainer never gets a cache hit and always
// rebuilds. A pure-Dockerfile build is reproducible by the probe, enabling a
// direct boot from the cached image. See coder/terraform-provider-envbuilder#68.

// 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,15 +21,15 @@
"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
// `groupmod: GID '1001' already exists`, breaking `devcontainer up`.
// (envbuilder ignores this key; it only affects the devcontainer CLI.)
// Don't remap the container user's UID/GID to match the host user. The
// openssh-server package 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 `groupmod: GID '1001' already exists`, breaking
// `devcontainer up`. (envbuilder ignores this key; it only affects the CLI.)
"updateRemoteUserUID": false,

// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Tooling is installed in the Dockerfile (see .devcontainer/Dockerfile), not
// via devcontainer features.

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
Expand Down
27 changes: 18 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,27 @@ My personal devcontainer for VSCode.

- `mcr.microsoft.com/devcontainers/typescript-node:24-bookworm`

## Devcontainer features

- Go
- Ruby (via asdf)
- Rust
- sshd
## Toolchain

The toolchain is installed directly in [`.devcontainer/Dockerfile`](.devcontainer/Dockerfile)
rather than via devcontainer `features:`. Coder's envbuilder prebuilt-image cache
probe cannot reproduce feature layers (features install from
`/.envbuilder/features/...` during the build but `/tmp/...` during the probe), so a
feature-based devcontainer never gets a cache hit and rebuilds on every start. A
pure-Dockerfile build is reproducible by the probe, so a cached workspace can boot
directly from the prebuilt image. See
[coder/terraform-provider-envbuilder#68](https://github.com/coder/terraform-provider-envbuilder/issues/68).

- Go (`1.26.3`)
- Ruby (`4.0.5`, built with ruby-build)
- Rust (stable, via rustup)
- sshd (port 2222)
- kubectl / helm / minikube
- GitHub CLI (`gh`)
- Azure CLI (`az`, latest)
- GitHub Copilot CLI
- Anthropic Claude Code
- opencode
- GitHub Copilot CLI (`@github/copilot`)
- Anthropic Claude Code (`@anthropic-ai/claude-code`)
- opencode (`opencode-ai`)
- `@openprose/prose-cli` (npm)
- Namespace CLI (`nsc`)
- Playwright with the Chromium browser (and required OS dependencies)
Expand Down