Skip to content

Commit 3ae1850

Browse files
committed
Add migration guide for transitioning to new devcontainer conventions
1 parent 7344fd3 commit 3ae1850

2 files changed

Lines changed: 278 additions & 110 deletions

File tree

docs/migration-guide.md

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
# Devcontainer Migration Guide
2+
3+
Guide to migrate an existing
4+
`.devcontainer/` setup onto the SoureCode conventions: persistent per-user
5+
home, dynamic user construct, and toolchain installed via features instead
6+
of inline Dockerfile install steps.
7+
8+
## Goals of the migration
9+
10+
1. Every devcontainer for the same Coder user shares one named volume mounted
11+
on the user's home directory. Bash history, git config, shell dotfiles,
12+
tool caches, Claude config, etc. persist across devcontainer rebuilds
13+
**and** across different projects.
14+
2. The container user / UID / GID is driven by `DEVCONTAINER_*` env vars so
15+
the same devcontainer works inside Coder (where the outer template sets
16+
these) and outside (sensible defaults).
17+
3. Toolchain installs (cmake, LLVM, sccache, Claude Code, RTK, …) come from
18+
published devcontainer features, not inline Dockerfile commands. The
19+
Dockerfile shrinks to base OS + common utilities + user creation.
20+
21+
## Input state (what you'll typically see)
22+
23+
An existing devcontainer that:
24+
25+
- Runs as `root` or a user named `vscode` / `ubuntu` / whatever the base
26+
image ships.
27+
- Has a Dockerfile with long inline installs for cmake / LLVM / sccache /
28+
Claude Code / RTK.
29+
- Uses ephemeral volumes or `${devcontainerId}`-scoped volumes (state dies
30+
on rebuild).
31+
- Hard-codes paths like `/root/.claude`, `/root/.cache/sccache`.
32+
33+
## Output state (what you're producing)
34+
35+
### `.devcontainer/devcontainer.json`
36+
37+
```jsonc
38+
{
39+
"name": "<project-name>",
40+
"build": {
41+
"dockerfile": "Dockerfile",
42+
"args": {
43+
"USERNAME": "${localEnv:DEVCONTAINER_USERNAME:dev}",
44+
"USER_UID": "${localEnv:DEVCONTAINER_USER_UID:1000}",
45+
"USER_GID": "${localEnv:DEVCONTAINER_USER_GID:1000}"
46+
}
47+
},
48+
"remoteUser": "${localEnv:DEVCONTAINER_USERNAME:dev}",
49+
"containerUser": "${localEnv:DEVCONTAINER_USERNAME:dev}",
50+
"mounts": [
51+
"source=devhome-${localEnv:OWNER_USERNAME:shared},target=/home/${localEnv:DEVCONTAINER_USERNAME:dev},type=volume"
52+
],
53+
"features": {
54+
// pick from the feature reference below
55+
},
56+
"customizations": {
57+
// keep whatever IDE config the project already has
58+
}
59+
}
60+
```
61+
62+
Rules:
63+
64+
- `name` — keep the project's existing name.
65+
- Volume name is **always** `devhome-${localEnv:OWNER_USERNAME:shared}`. Do
66+
**not** add the project name to the volume — the point is that every
67+
project shares one home per user. Fallback `:shared` kicks in when running
68+
the devcontainer locally outside Coder.
69+
- `remoteUser` and `containerUser` both use `${localEnv:DEVCONTAINER_USERNAME:dev}`.
70+
- Do **not** set `CLAUDE_CONFIG_DIR` or `SCCACHE_DIR` in `containerEnv`
71+
their defaults (`~/.claude`, `~/.cache/sccache`) already land inside the
72+
persistent home volume.
73+
- `customizations` — preserve whatever the project had (JetBrains backend,
74+
VS Code settings/extensions, etc.).
75+
76+
### `.devcontainer/Dockerfile`
77+
78+
```dockerfile
79+
FROM <base-image-and-digest>
80+
81+
ARG USERNAME
82+
ARG USER_UID=1000
83+
ARG USER_GID=${USER_UID}
84+
85+
ENV DEBIAN_FRONTEND=noninteractive
86+
ENV DEVCONTAINER=true
87+
88+
RUN apt-get update && \
89+
apt-get install -y --no-install-recommends \
90+
<base packages the project actually needs> \
91+
sudo ca-certificates curl git openssh-client && \
92+
apt-get clean && rm -rf /var/lib/apt/lists/* && \
93+
userdel -r ubuntu 2>/dev/null || true && \
94+
groupadd --gid "${USER_GID}" "${USERNAME}" && \
95+
useradd --uid "${USER_UID}" --gid "${USER_GID}" --create-home --shell /bin/bash "${USERNAME}" && \
96+
echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/"${USERNAME}" && \
97+
chmod 0440 /etc/sudoers.d/"${USERNAME}"
98+
99+
# Project-specific env, shell config, workdir, etc. go here.
100+
101+
USER ${USERNAME}
102+
```
103+
104+
Rules:
105+
106+
- Start with the project's existing `FROM`. Keep the digest pin if present.
107+
- Declare `ARG USERNAME` (no default — required from build.args), plus
108+
`ARG USER_UID=1000` and `ARG USER_GID=${USER_UID}`.
109+
- Create the user in the same `RUN` that does apt installs (fewer layers).
110+
`userdel -r ubuntu 2>/dev/null || true` covers Ubuntu base images that
111+
ship a default user. Use soft-fail on non-Ubuntu bases.
112+
- End with `USER ${USERNAME}`.
113+
- **Remove** any inline installs that are now handled by features
114+
(see next section). Also remove:
115+
- Manual `.bashrc` history-persistence hacks tied to `/commandhistory` or
116+
`/root/.bash_history` — home is on the volume now, bash history
117+
persists naturally at `~/.bash_history`.
118+
- Hard-coded `CC=clang-22` / `CXX=clang++-22` — the `llvm` feature sets
119+
these via `containerEnv`.
120+
- Hard-coded `SCCACHE_DIR` — default already works.
121+
- If the project sets shell-level config (`HISTSIZE`, `PATH` additions, etc.)
122+
put it in `/etc/bash.bashrc` (system-wide, sourced by non-login interactive
123+
bashes on Debian/Ubuntu) **not** `~/.bashrc`. The home dir is a volume —
124+
`~/.bashrc` only gets seeded on first-create and diverges from the image
125+
on rebuilds.
126+
127+
## Available features
128+
129+
Reference: `ghcr.io/sourecode/devcontainer-features/<id>:<major-version>`
130+
131+
| Feature | Purpose | Notable options |
132+
|---|---|---|
133+
| `cmake` | CMake from Kitware GitHub releases, distro-agnostic | `version` (default `latest`) |
134+
| `llvm` | Clang/LLVM via `apt.llvm.org`. Sets `CC`/`CXX` in containerEnv. | `version` (default `22`), `all` (default `true`) |
135+
| `sccache` | Mozilla sccache from GitHub releases | `version` (default `latest`) |
136+
| `nvm` | NVM + optional Node install | `version`, `node` (default `lts`) |
137+
| `claude-code` | Anthropic Claude Code CLI ||
138+
| `rtk` | RTK CLI ||
139+
| `context-mode` | Context-mode integration ||
140+
141+
### How to decide which features to add
142+
143+
Scan the original Dockerfile for inline installs and map them:
144+
145+
- `apt install cmake` (from any source) → drop, add `cmake` feature.
146+
- `apt.llvm.org/llvm.sh` / `apt install clang` / `apt install clangd` /
147+
any llvm-* packages → drop, add `llvm` feature (it installs the full
148+
toolchain with `all: true`).
149+
- `sccache` tarball install → drop, add `sccache` feature.
150+
- `curl .../claude-code install` → drop, add `claude-code` feature.
151+
- `curl .../rtk install` → drop, add `rtk` feature.
152+
- Anything NOT covered by a feature stays in the Dockerfile.
153+
154+
Typical `features` block for a C++ project:
155+
156+
```jsonc
157+
"features": {
158+
"ghcr.io/sourecode/devcontainer-features/cmake:1": {},
159+
"ghcr.io/sourecode/devcontainer-features/llvm:1": {},
160+
"ghcr.io/sourecode/devcontainer-features/sccache:1": {},
161+
"ghcr.io/sourecode/devcontainer-features/claude-code:1": {},
162+
"ghcr.io/sourecode/devcontainer-features/rtk:1": {},
163+
"ghcr.io/sourecode/devcontainer-features/context-mode:1": {}
164+
}
165+
```
166+
167+
## Where env vars come from
168+
169+
- `DEVCONTAINER_USERNAME`, `DEVCONTAINER_USER_UID`, `DEVCONTAINER_USER_GID`,
170+
`OWNER_USERNAME` are set on the outer Coder workspace agent
171+
(`coder_agent.main.env` in the Coder template). The `@devcontainers/cli`
172+
process inherits them, so `${localEnv:*}` in `devcontainer.json` resolves
173+
at devcontainer startup.
174+
- Running the devcontainer locally on a laptop (no Coder): the `:fallback`
175+
defaults in each `${localEnv:NAME:fallback}` kick in. User ends up as
176+
`dev` at 1000:1000, volume is `devhome-shared`.
177+
178+
## Persistent home caveats
179+
180+
One volume per Coder user, shared across every devcontainer they open. This
181+
is intentional — same bash history, same `~/.gitconfig`, same `~/.claude`
182+
everywhere. Side effects to understand:
183+
184+
- First-time volume creation seeds from the image's `/home/<user>` (Docker
185+
copies dir contents when an empty named volume is mounted over a
186+
non-empty target). Subsequent rebuilds use the volume; changes to the
187+
image's home dir will **not** propagate.
188+
- Put long-lived shell config in `/etc/bash.bashrc` (image-side) so it
189+
stays authoritative.
190+
- Tools that store env-specific state in `$HOME` (some pyenv/nvm layouts,
191+
`.cache/` bloat) can collide across projects. Usually fine for dotfiles
192+
and coarse caches; tune per-project if something misbehaves.
193+
194+
## Checklist for the migrating agent
195+
196+
1. Edit `.devcontainer/devcontainer.json` to match the template above.
197+
Preserve `name` and `customizations` from the original.
198+
2. Edit `.devcontainer/Dockerfile`:
199+
- Add `ARG USERNAME / USER_UID / USER_GID`.
200+
- Fold user creation into the apt `RUN`.
201+
- Remove inline installs that are covered by features.
202+
- Move any `.bashrc` shell config to `/etc/bash.bashrc`.
203+
- End with `USER ${USERNAME}`.
204+
3. Remove obsolete files: standalone `bashrc`-patching scripts,
205+
`/commandhistory` directory hacks, hard-coded `CLAUDE_CONFIG_DIR` /
206+
`SCCACHE_DIR` env vars.
207+
4. Build-test the devcontainer locally:
208+
`devcontainer up --workspace-folder .` → should succeed, drop into the
209+
non-root user's shell, and have `cmake`, `clang`, `sccache`, `claude`,
210+
`rtk` on `$PATH`.
211+
5. Commit.

0 commit comments

Comments
 (0)