11# Persisting state across devcontainer rebuilds
22
3- Our convention: mount a single named volume at the user's home directory,
4- scoped per-Coder-user, shared across every devcontainer that user opens.
5- Everything that lives under ` $HOME ` persists automatically — bash history,
6- git config, SSH keys, tool caches, Claude Code state, sccache cache, pipx
7- installs, anything.
3+ Our convention: a single per-owner home volume, bind-mounted through the
4+ outer Coder workspace into every devcontainer the owner opens. Everything
5+ under ` $HOME ` persists automatically — bash history, git config, SSH keys,
6+ tool caches, Claude Code state, sccache cache, pipx installs, anything —
7+ and it persists * across workspaces* , not just across rebuilds of one
8+ workspace.
89
910## The mount
1011
@@ -13,20 +14,60 @@ In every `.devcontainer/devcontainer.json`:
1314``` jsonc
1415{
1516 " mounts" : [
16- " source=devhome-${localEnv:OWNER_USERNAME:shared},target=/home/${localEnv:DEVCONTAINER_USERNAME:dev},type=volume"
17- ]
17+ " source=/mnt/devhome,target=/home/${localEnv:DEVCONTAINER_USERNAME:dev},type=bind"
18+ ],
19+ " onCreateCommand" : " test -z \" $(ls -A $HOME 2>/dev/null)\" && cp -rT /etc/skel $HOME || true"
1820}
1921```
2022
21- - ` OWNER_USERNAME ` is injected by the Coder template ( ` coder_agent.main.env ` )
22- and resolves to the Coder workspace owner's username .
23- - ` DEVCONTAINER_USERNAME ` is the in-container user (also from
23+ - ` /mnt/devhome ` is a stable path inside the outer Coder workspace where
24+ the per-owner home volume is mounted (see Topology below) .
25+ - ` DEVCONTAINER_USERNAME ` is the in-container user (from
2426 ` coder_agent.main.env ` ; default ` dev ` ).
25- - Running outside Coder, both fall back to sensible defaults —
26- ` devhome-shared ` mounted at ` /home/dev ` .
27+ - ` onCreateCommand ` seeds the home from ` /etc/skel ` on first-ever attach.
28+ Bind mounts don't auto-seed from the image the way named volumes do, so
29+ the command copies skeleton dotfiles on the first workspace per owner
30+ and no-ops on every subsequent create.
31+ - Running outside Coder, change the mount ` source ` to a local path or a
32+ named volume — the rest of the devcontainer stays the same.
2733
28- One volume per Coder user. Open three different project devcontainers and
29- they all see the same home.
34+ One volume per Coder user. Open three different project devcontainers,
35+ open them from three different workspaces — they all see the same home.
36+
37+ ## Topology
38+
39+ ```
40+ ┌───────────────────────────────────────────────────────────────────┐
41+ │ host dockerd │
42+ │ │
43+ │ docker volume: coder-<owner>-devhome ◄──── one per owner │
44+ │ │ │
45+ │ ├─► outer workspace A at /mnt/devhome │
46+ │ │ └─► inner devcontainer (bind) /home/<dev-user> │
47+ │ │ │
48+ │ ├─► outer workspace B at /mnt/devhome │
49+ │ │ └─► inner devcontainer (bind) /home/<dev-user> │
50+ │ │ │
51+ │ └─► outer workspace C at /mnt/devhome │
52+ │ └─► inner devcontainer (bind) /home/<dev-user> │
53+ └───────────────────────────────────────────────────────────────────┘
54+ ```
55+
56+ The volume lives in the ** host** dockerd, above any individual workspace.
57+ It's defined in the Coder template as ` docker_volume "devhome" ` , scoped
58+ by ` coder_workspace_owner.me.name ` , and bind-mounted into the outer
59+ workspace container at ` /mnt/devhome ` . Each inner devcontainer then
60+ bind-mounts that path as its own ` /home/<user> ` , so the filesystem every
61+ workspace's IDE and shell see is literally the same volume.
62+
63+ Properties that fall out of this layout:
64+
65+ - Survives workspace deletion. Blowing away a workspace doesn't touch
66+ ` coder-<owner>-devhome ` .
67+ - Survives ` rebuild_no_cache ` . That parameter only drops inner dockerd
68+ state (` vsc-* ` images + BuildKit cache) — not the host-level volume.
69+ - Nothing in ` docs/migration-guide.md ` 's volume name changes when you
70+ rename a workspace. Identity is tied to the owner, not the workspace.
3071
3172## What persists automatically
3273
@@ -43,55 +84,78 @@ Any path under `$HOME`. A non-exhaustive list that matters for us:
4384| ` ~/.local/ ` | pipx installs, user-local binaries |
4485| ` ~/.config/ ` | Per-app config |
4586
46- Nothing extra to configure. ` SCCACHE_DIR ` and ` CLAUDE_CONFIG_DIR ` default to
47- paths under ` $HOME ` , so the env vars the old pattern used to override those
48- are unnecessary and should be removed.
87+ Nothing extra to configure. ` SCCACHE_DIR ` and ` CLAUDE_CONFIG_DIR ` default
88+ to paths under ` $HOME ` , so the env vars the old pattern used to override
89+ them are unnecessary and should be removed.
4990
5091## What doesn't persist
5192
52- - Anything ** outside** ` $HOME ` — ` /usr/local/... ` , ` /opt/... ` , etc. These come
53- from the image, the Dockerfile, or feature installs. Rebuild the image to
54- change them.
55- - The workspace folder itself (usually ` /workspace ` ). This is bind-mounted
56- from the outer Coder workspace container's ` /home/<coder>/<repo> ` , which
57- has its own persistence (via the outer ` docker_volume.home_volume ` in the
58- Coder template). Your git working tree survives outer-workspace restarts;
59- inside the devcontainer it's the same filesystem surface.
93+ - Anything ** outside** ` $HOME ` — ` /usr/local/... ` , ` /opt/... ` , etc. These
94+ come from the image, the Dockerfile, or feature installs. Rebuild the
95+ image to change them. This is also why our features install to
96+ ` /usr/local/bin ` rather than ` ~/.local/bin ` : feature-installed binaries
97+ need to live outside the home volume.
98+ - The workspace folder itself (usually ` /workspaces/<repo> ` ). That's the
99+ git clone under the outer workspace's ` /home/coder ` , persisted by the
100+ per-workspace ` docker_volume.home_volume ` . Your working tree survives
101+ restarts of * its* workspace, but is scoped to that workspace.
102+
103+ ## Seeding on first create
104+
105+ Bind mounts don't auto-seed from the image. The first time the per-owner
106+ volume is attached, it's empty, and the devcontainer's image-side
107+ ` /home/<user> ` (skeleton dotfiles, defaults from ` /etc/skel ` ) is * not*
108+ copied into it automatically.
60109
61- ## First-create seeding
110+ The ` onCreateCommand ` above handles seeding explicitly:
111+
112+ ``` bash
113+ test -z " $( ls -A $HOME 2> /dev/null) " && cp -rT /etc/skel $HOME || true
114+ ```
62115
63- When Docker mounts an empty named volume over a non- empty directory, it
64- copies the image's directory contents into the volume. So the first time a
65- user opens any devcontainer, the image's ` /home/<user> ` (skeleton dotfiles,
66- defaults from ` /etc/skel ` ) seeds the volume .
116+ - On the first-ever create for an owner, ` $HOME ` is empty → copy
117+ ` /etc/skel ` in.
118+ - On every subsequent create ( any workspace, any rebuild), ` $HOME ` has
119+ content → no-op .
67120
68- After that, the volume wins. Subsequent rebuilds of the image will ** not**
69- propagate changes to the image's ` $HOME ` into the existing volume.
121+ After the first create the volume wins on its own. Subsequent rebuilds
122+ of the image will ** not** propagate changes to the image's ` $HOME ` into
123+ the existing volume.
70124
71- Consequence: don't put long-lived shell config in ` ~/.bashrc ` in the
72- Dockerfile — it'd be stuck at whatever got seeded on first- create. Use
73- ` /etc/bash.bashrc ` instead (system-wide, sourced by non-login interactive
74- bashes on Debian/Ubuntu, image-owned, always authoritative).
125+ Consequence: don't put long-lived shell config in ` ~/.bashrc ` via the
126+ Dockerfile — it'd be stuck at whatever got seeded on the first create.
127+ Use ` /etc/bash.bashrc ` instead (system-wide, sourced by non-login
128+ interactive bashes on Debian/Ubuntu, image-owned, always authoritative).
75129
76- ## Cross-project side effects
130+ ## Cross-workspace side effects
77131
78- One home for every devcontainer means:
132+ One home for every workspace means:
79133
80134- Pros: universal ` ~/.gitconfig ` , ` ~/.ssh/* ` , one Claude Code login, one
81- bash history, shared sccache/` ~/.cargo ` caches.
135+ shared bash history, shared sccache / ` ~/.cargo ` caches across all
136+ projects the owner opens.
82137- Cons: tools that write env-specific state to ` $HOME ` (some pyenv / nvm
83138 layouts, project-specific ` ~/.config/* ` files) will leak between
84139 projects. For most dotfile-level state this is exactly what you want.
85140 If a specific tool misbehaves, override its config dir via ` containerEnv `
86- to point at a project-scoped path under the repo.
141+ to point at a project-scoped path under the repo's working tree.
142+ - Running two workspaces simultaneously means two processes writing to
143+ the same home — the same situation as running two terminals on your
144+ laptop. Tools that support concurrent writers (bash history with
145+ ` histappend ` , content-addressed caches, atomic-rename lockfiles) cope
146+ fine. Tools that don't (single-writer state files) behave as they would
147+ locally.
87148
88149## Resetting a home
89150
90151If the shared home gets into a bad state, nuke the volume from the host:
91152
92153``` bash
93- docker volume rm devhome -< owner-username>
154+ docker volume rm coder -< owner-username> -devhome
94155```
95156
96- Next devcontainer start re-seeds it from the image. You'll need to
97- re-login to Claude Code, re-set any interactive state, etc.
157+ Next devcontainer start sees an empty ` /mnt/devhome ` , the ` onCreateCommand `
158+ re-seeds it from ` /etc/skel ` , and you're back to a clean home. You'll need
159+ to re-login to Claude Code, re-populate ` ~/.ssh ` (the Coder sub-agent does
160+ this automatically via ` coder_script "git_ssh_signing" ` ), and re-set any
161+ interactive state.
0 commit comments