Skip to content

Commit 941cd8a

Browse files
chapterjasonclaude
andcommitted
User-context installers, drop PATH symlinks and systemd / chown plumbing
Everything that targets a single user (nvm, claude-code, rtk, web-shell on base; cmake, sccache on cpp) now runs as `coder` from a user-context loop in the Dockerfile. Upstream installers write into $HOME directly — no root+chown dance, no /usr/local symlinks into user-space. Script-by-script changes: scripts/nvm/install.sh - Install to $HOME/.nvm via upstream (no more /usr/local/share/nvm). - Let the upstream installer append its loader snippet to ~/.profile (PROFILE=$HOME/.profile). - Dropped /etc/profile.d/nvm.sh, /etc/bash.bashrc and /etc/zsh/zshrc hooks, and all chown/chmod plumbing. scripts/claude-code/install.sh (earlier commit, context here) - Runs as user, appends a runtime-idempotent PATH hook to ~/.profile for $HOME/.local/bin. scripts/rtk/install.sh - Use plain upstream install, which lands the binary in $HOME/.local/bin. - post-create.sh moves from /usr/local/share/rtk to $HOME/.local/share/rtk. scripts/sccache/install.sh - Pipe curl | tar with --strip-components=1 + path filter to extract just the binary straight into $HOME/.local/bin. No mktemp, no trap. scripts/cmake/install.sh (earlier commit, context here) - Merge tarball into $HOME/.local with --strip-components=1. scripts/web-shell/install.sh - Drop the systemd unit + /etc/profile.d fallback + INIT_COMM branch. - Drop the $HOME/.cache chown repair. - Drop the curl+tmpfile dance — npm install -g accepts the GitHub release tarball URL directly. scripts/llvm/install.sh, context-mode, home-persist - Drop the per-script `if ! command -v X; then apt-get install X` fallbacks. curl / tar / git / jq / ca-certificates are now explicitly in the base image's main apt install. src/base/Dockerfile - Add ca-certificates and curl to the main apt install. - Root loop: context-mode, home-persist only. - User-context loop: nvm, claude-code, rtk, web-shell. src/cpp/Dockerfile - Root loop: just llvm. - User-context loop: cmake + sccache. main.tf - coder_script.web_shell now sources $HOME/.nvm/nvm.sh + nvm use default so the backgrounded web-shell binary resolves on PATH. - Replace the systemd-based web-shell supervision with a plain coder_script: `nohup web-shell … &`, no while-loop, no pgrep guard — matches coder/code-server and coder/jetbrains module patterns. - Lifecycle_init references $HOME/.local/share/rtk/post-create.sh. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4bf74f2 commit 941cd8a

11 files changed

Lines changed: 91 additions & 296 deletions

File tree

main.tf

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ data "coder_parameter" "directory" {
6363
type = "string"
6464
name = "directory"
6565
display_name = "Working directory"
66-
description = "Folder IDE modules open by default."
67-
default = "/home/coder"
66+
description = "Folder IDE modules and web-shell open by default."
67+
default = "/home/coder/projects"
6868
mutable = true
6969
}
7070

@@ -94,12 +94,6 @@ resource "coder_agent" "main" {
9494
startup_script = <<-EOT
9595
set -e
9696
97-
# Prepare user home with default files on first start.
98-
if [ ! -f ~/.init_done ]; then
99-
cp -rT /etc/skel ~
100-
touch ~/.init_done
101-
fi
102-
10397
# SSH key for git-over-SSH clones. Public key + allowed_signers come via
10498
# coder_script.git_ssh_signing below.
10599
mkdir -p ~/.ssh && chmod 700 ~/.ssh
@@ -209,6 +203,25 @@ resource "coder_app" "web-shell" {
209203
}
210204
}
211205

206+
# Start web-shell on agent start. web-shell lives in the user's nvm default
207+
# node bin, so load nvm and activate the default alias to put it on PATH.
208+
resource "coder_script" "web_shell" {
209+
count = data.coder_workspace.me.start_count
210+
agent_id = coder_agent.main.id
211+
display_name = "web-shell"
212+
icon = "/icon/terminal.svg"
213+
run_on_start = true
214+
script = <<-EOT
215+
#!/usr/bin/env bash
216+
set -e
217+
export NVM_DIR="$HOME/.nvm"
218+
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && nvm use default >/dev/null 2>&1 || true
219+
HOST=127.0.0.1 PORT=4000 WEB_SHELL_CWD="${data.coder_parameter.directory.value}" \
220+
nohup web-shell > /tmp/web-shell.log 2>&1 &
221+
disown >/dev/null 2>&1 || true
222+
EOT
223+
}
224+
212225
# See https://registry.coder.com/modules/coder/jetbrains
213226
module "jetbrains" {
214227
count = data.coder_workspace.me.start_count
@@ -224,7 +237,7 @@ module "git-clone" {
224237
source = "registry.coder.com/coder/git-clone/coder"
225238
agent_id = coder_agent.main.id
226239
url = data.coder_parameter.repo_url.value
227-
base_dir = "~"
240+
base_dir = "~/projects"
228241
version = "~> 1.0"
229242
}
230243

@@ -331,7 +344,7 @@ resource "coder_script" "lifecycle_init" {
331344
332345
[ -x /usr/local/bin/home-persist-resolve ] && /usr/local/bin/home-persist-resolve
333346
[ -x /usr/local/share/context-mode/post-create.sh ] && /usr/local/share/context-mode/post-create.sh
334-
[ -x /usr/local/share/rtk/post-create.sh ] && /usr/local/share/rtk/post-create.sh
347+
[ -x "$HOME/.local/share/rtk/post-create.sh" ] && "$HOME/.local/share/rtk/post-create.sh"
335348
exit 0
336349
EOT
337350
}
@@ -362,10 +375,12 @@ resource "docker_volume" "docker_data" {
362375
}
363376
}
364377

365-
# Per-workspace $HOME volume. Persists user data (~/.bashrc tweaks, cloned
366-
# repo, build artefacts) across workspace restarts.
367-
resource "docker_volume" "home_volume" {
368-
name = "coder-${data.coder_workspace.me.id}-home"
378+
# Per-workspace projects volume. Cloned repos + work-in-progress live here
379+
# so they survive workspace restarts. $HOME itself is image-owned and resets
380+
# each start; per-owner state that must persist outside projects goes through
381+
# home-persist (see docs/persistence.md).
382+
resource "docker_volume" "projects_volume" {
383+
name = "coder-${data.coder_workspace.me.id}-projects"
369384
lifecycle {
370385
ignore_changes = all
371386
}
@@ -418,8 +433,8 @@ resource "docker_container" "workspace" {
418433
}
419434

420435
volumes {
421-
container_path = local.workspace_home
422-
volume_name = docker_volume.home_volume.name
436+
container_path = "${local.workspace_home}/projects"
437+
volume_name = docker_volume.projects_volume.name
423438
read_only = false
424439
}
425440

scripts/cmake/install.sh

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
11
#!/usr/bin/env bash
2-
# cmake installer.
2+
# cmake installer. Runs as the workspace user — binaries land in $HOME/.local/.
33
# https://cmake.org/
44
set -e
55

66
CMAKE_VERSION="${VERSION:-latest}"
77

8-
if ! command -v curl >/dev/null 2>&1 || ! command -v tar >/dev/null 2>&1; then
9-
apt-get update
10-
apt-get install -y --no-install-recommends curl ca-certificates tar
11-
rm -rf /var/lib/apt/lists/*
12-
fi
13-
148
if [ "$CMAKE_VERSION" = "latest" ]; then
159
CMAKE_VERSION="$(curl -fsSL https://api.github.com/repos/Kitware/CMake/releases/latest \
1610
| sed -n 's/^.*"tag_name": *"v\([^"]*\)".*$/\1/p')"
@@ -28,13 +22,8 @@ trap 'rm -rf "$TMPDIR"' EXIT
2822

2923
curl -fsSL -o "$TMPDIR/cmake.tar.gz" \
3024
"https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/cmake-${CMAKE_VERSION}-linux-${CM_ARCH}.tar.gz"
31-
tar -xzf "$TMPDIR/cmake.tar.gz" -C "$TMPDIR"
32-
33-
INSTALL_DIR="/usr/local/cmake-${CMAKE_VERSION}"
34-
rm -rf "$INSTALL_DIR"
35-
mv "$TMPDIR/cmake-${CMAKE_VERSION}-linux-${CM_ARCH}" "$INSTALL_DIR"
3625

37-
for bin in cmake ctest cpack ccmake; do
38-
[ -x "$INSTALL_DIR/bin/$bin" ] || continue
39-
ln -sf "$INSTALL_DIR/bin/$bin" "/usr/local/bin/$bin"
40-
done
26+
# Merge tarball contents into $HOME/.local — binaries land at $HOME/.local/bin/,
27+
# which is on PATH via claude-code's .profile hook. No symlinks.
28+
mkdir -p "$HOME/.local"
29+
tar -xzf "$TMPDIR/cmake.tar.gz" -C "$HOME/.local" --strip-components=1

scripts/context-mode/install.sh

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,6 @@
99
# so it writes through the symlink into the persistent volume on every start.
1010
set -e
1111

12-
if ! command -v git >/dev/null 2>&1; then
13-
apt-get update
14-
apt-get install -y --no-install-recommends git ca-certificates
15-
rm -rf /var/lib/apt/lists/*
16-
fi
17-
1812
mkdir -p /usr/local/share/context-mode
1913
cat >/usr/local/share/context-mode/post-create.sh <<'EOF'
2014
#!/usr/bin/env bash

scripts/home-persist/install.sh

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,5 @@
88
# Coder parameter.
99
set -e
1010

11-
if ! command -v jq >/dev/null 2>&1; then
12-
apt-get update
13-
apt-get install -y --no-install-recommends jq ca-certificates
14-
rm -rf /var/lib/apt/lists/*
15-
fi
16-
1711
mkdir -p /etc/home-persist.d
1812
install -m 0755 "$(dirname "$0")/resolve.sh" /usr/local/bin/home-persist-resolve

scripts/llvm/install.sh

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,6 @@ set -e
66
LLVM_VERSION="${VERSION:-22}"
77
LLVM_ALL="${ALL:-true}"
88

9-
# llvm.sh writes the LLVM apt source in deb822 format on trixie/forky/sid, so
10-
# software-properties-common (gone from trixie) is no longer required there.
11-
# Older Debian/Ubuntu suites still need add-apt-repository, which lives in
12-
# software-properties-common. Detect the suite and preinstall accordingly.
13-
PKGS=(ca-certificates curl gnupg lsb-release wget)
14-
. /etc/os-release
15-
case "${VERSION_CODENAME:-}" in
16-
trixie|forky|sid) ;;
17-
*) PKGS+=(software-properties-common) ;;
18-
esac
19-
NEED=()
20-
for pkg in "${PKGS[@]}"; do
21-
dpkg -s "$pkg" >/dev/null 2>&1 || NEED+=("$pkg")
22-
done
23-
if [ "${#NEED[@]}" -gt 0 ]; then
24-
apt-get update
25-
apt-get install -y --no-install-recommends "${NEED[@]}"
26-
rm -rf /var/lib/apt/lists/*
27-
fi
28-
299
TMPDIR="$(mktemp -d)"
3010
trap 'rm -rf "$TMPDIR"' EXIT
3111

scripts/nvm/install.sh

Lines changed: 12 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,25 @@
11
#!/usr/bin/env bash
2-
# nvm installer.
2+
# nvm installer. Runs as the workspace user — installs to $HOME/.nvm via
3+
# the upstream installer, which appends its loader snippet to ~/.profile.
34
# https://github.com/nvm-sh/nvm
45
set -e
56

67
NVM_VERSION="${VERSION:-0.40.4}"
78
NODE_VERSION="${NODE:-lts}"
89

9-
NVM_DIR=/usr/local/share/nvm
10-
export NVM_DIR
11-
12-
USER_NAME="${_REMOTE_USER:-${USERNAME:-root}}"
13-
if [ "$USER_NAME" = "root" ]; then
14-
USER_GROUP="root"
15-
else
16-
USER_GROUP="$(id -gn "$USER_NAME")"
17-
fi
18-
19-
if ! command -v curl >/dev/null 2>&1; then
20-
apt-get update
21-
apt-get install -y --no-install-recommends curl ca-certificates
22-
rm -rf /var/lib/apt/lists/*
23-
fi
24-
25-
mkdir -p "$NVM_DIR"
26-
chown "$USER_NAME:$USER_GROUP" "$NVM_DIR"
27-
chmod g+ws "$NVM_DIR"
28-
29-
curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh" | PROFILE=/dev/null bash
30-
31-
cat >/etc/profile.d/nvm.sh <<EOF
32-
export NVM_DIR="$NVM_DIR"
33-
if [ -s "\$NVM_DIR/nvm.sh" ]; then
34-
\\. "\$NVM_DIR/nvm.sh"
35-
[ -s "\$NVM_DIR/bash_completion" ] && \\. "\$NVM_DIR/bash_completion"
36-
# Activate the default alias if present — puts node/npm/npx on PATH.
37-
[ -s "\$NVM_DIR/alias/default" ] && nvm use default >/dev/null 2>&1
38-
fi
39-
EOF
40-
chmod 644 /etc/profile.d/nvm.sh
41-
42-
# Non-login interactive bash shells (VS Code / code-server terminals) source
43-
# /etc/bash.bashrc, not /etc/profile.d. Without this hook, the `nvm` shell
44-
# function isn't defined in those shells even though `node` / `npm` work via
45-
# the /usr/local/bin symlinks below. Idempotent guard so re-running the
46-
# install doesn't duplicate the block.
47-
if [ ! -f /etc/bash.bashrc ] || ! grep -q 'nvm-hook' /etc/bash.bashrc; then
48-
cat >>/etc/bash.bashrc <<'EOF'
49-
50-
# nvm-hook: sourced by non-login interactive bash shells.
51-
if [ -z "${NVM_DIR:-}" ] && [ -s /etc/profile.d/nvm.sh ]; then
52-
. /etc/profile.d/nvm.sh
53-
fi
54-
EOF
55-
fi
56-
57-
# Same for zsh if the distro ships /etc/zsh/zshrc.
58-
if [ -f /etc/zsh/zshrc ] && ! grep -q 'nvm-hook' /etc/zsh/zshrc; then
59-
cat >>/etc/zsh/zshrc <<'EOF'
60-
61-
# nvm-hook: sourced by interactive zsh shells.
62-
if [ -z "${NVM_DIR:-}" ] && [ -s /etc/profile.d/nvm.sh ]; then
63-
. /etc/profile.d/nvm.sh
64-
fi
65-
EOF
66-
fi
10+
curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh" \
11+
| PROFILE="$HOME/.profile" bash
6712

6813
if [ "$NODE_VERSION" != "none" ]; then
14+
export NVM_DIR="$HOME/.nvm"
15+
# shellcheck disable=SC1091
16+
. "$NVM_DIR/nvm.sh"
6917
if [ "$NODE_VERSION" = "lts" ]; then
70-
NVM_INSTALL_ARG="--lts"
71-
NVM_ALIAS_TARGET="lts/*"
18+
nvm install --lts
19+
nvm alias default "lts/*"
7220
else
73-
NVM_INSTALL_ARG="$NODE_VERSION"
74-
NVM_ALIAS_TARGET="$NODE_VERSION"
21+
# shellcheck disable=SC2086
22+
nvm install "$NODE_VERSION"
23+
nvm alias default "$NODE_VERSION"
7524
fi
76-
77-
# shellcheck disable=SC1091
78-
. "$NVM_DIR/nvm.sh"
79-
# shellcheck disable=SC2086
80-
nvm install $NVM_INSTALL_ARG
81-
nvm alias default "$NVM_ALIAS_TARGET"
8225
fi
83-
84-
chown -R "$USER_NAME:$USER_GROUP" "$NVM_DIR"

scripts/rtk/install.sh

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,17 @@
11
#!/usr/bin/env bash
2-
# rtk installer.
2+
# rtk installer. Runs as the workspace user — binary lands in $HOME/.local/bin.
33
# https://github.com/rtk-ai/rtk
4-
#
5-
# Our workspaces mount $HOME as a named volume (see docs/persistence.md),
6-
# so anything written to the user's home at build time is hidden on runs
7-
# where the volume already has contents. Install the binary system-wide to
8-
# /usr/local/bin, and defer the `rtk init` auto-patch to a post-create hook
9-
# (run via coder_script at workspace start) so it runs against the real home.
104
set -e
115

12-
if ! command -v curl >/dev/null 2>&1; then
13-
apt-get update
14-
apt-get install -y --no-install-recommends curl ca-certificates
15-
rm -rf /var/lib/apt/lists/*
16-
fi
17-
18-
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh |
19-
RTK_INSTALL_DIR=/usr/local/bin sh
6+
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh
207

21-
if [ ! -x /usr/local/bin/rtk ]; then
22-
echo "rtk: expected /usr/local/bin/rtk after install, but it was not found." >&2
8+
if [ ! -x "$HOME/.local/bin/rtk" ]; then
9+
echo "rtk: expected $HOME/.local/bin/rtk after install, but it was not found." >&2
2310
exit 1
2411
fi
2512

26-
mkdir -p /usr/local/share/rtk
27-
cat >/usr/local/share/rtk/post-create.sh <<'EOF'
13+
mkdir -p "$HOME/.local/share/rtk"
14+
cat >"$HOME/.local/share/rtk/post-create.sh" <<'EOF'
2815
#!/usr/bin/env bash
2916
# Runs as the remote user via a coder_script at workspace start.
3017
set -e
@@ -37,4 +24,4 @@ fi
3724
mkdir -p "$HOME/.claude"
3825
rtk init -g --auto-patch
3926
EOF
40-
chmod 0755 /usr/local/share/rtk/post-create.sh
27+
chmod 0755 "$HOME/.local/share/rtk/post-create.sh"

scripts/sccache/install.sh

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
11
#!/usr/bin/env bash
2-
# sccache installer.
2+
# sccache installer. Runs as the workspace user — binary lands in $HOME/.local/bin.
33
# https://github.com/mozilla/sccache
44
set -e
55

66
SCCACHE_VERSION="${VERSION:-latest}"
77

8-
if ! command -v curl >/dev/null 2>&1 || ! command -v tar >/dev/null 2>&1; then
9-
apt-get update
10-
apt-get install -y --no-install-recommends curl ca-certificates tar
11-
rm -rf /var/lib/apt/lists/*
12-
fi
13-
148
if [ "$SCCACHE_VERSION" = "latest" ]; then
159
SCCACHE_VERSION="$(curl -fsSL https://api.github.com/repos/mozilla/sccache/releases/latest \
1610
| sed -n 's/^.*"tag_name": *"v\([^"]*\)".*$/\1/p')"
@@ -23,11 +17,7 @@ case "$ARCH" in
2317
*) echo "sccache: unsupported arch: $ARCH" >&2; exit 1 ;;
2418
esac
2519

26-
TMPDIR="$(mktemp -d)"
27-
trap 'rm -rf "$TMPDIR"' EXIT
28-
29-
curl -fsSL -o "$TMPDIR/sccache.tar.gz" \
30-
"https://github.com/mozilla/sccache/releases/download/v${SCCACHE_VERSION}/sccache-v${SCCACHE_VERSION}-${SC_ARCH}.tar.gz"
31-
tar -xzf "$TMPDIR/sccache.tar.gz" -C "$TMPDIR"
32-
33-
install -m 0755 "$TMPDIR/sccache-v${SCCACHE_VERSION}-${SC_ARCH}/sccache" /usr/local/bin/sccache
20+
mkdir -p "$HOME/.local/bin"
21+
curl -fsSL "https://github.com/mozilla/sccache/releases/download/v${SCCACHE_VERSION}/sccache-v${SCCACHE_VERSION}-${SC_ARCH}.tar.gz" \
22+
| tar -xzf - -C "$HOME/.local/bin" --strip-components=1 \
23+
"sccache-v${SCCACHE_VERSION}-${SC_ARCH}/sccache"

0 commit comments

Comments
 (0)