Repository navigation
Expand file tree
/
Copy pathdevcontainer.json
More file actions
182 lines (177 loc) · 10.6 KB
/
Copy pathdevcontainer.json
File metadata and controls
182 lines (177 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
{
"name": "devlaunch",
"build": {
"dockerfile": "Dockerfile",
// `.devcontainer/`, not the repository root, and the change is
// load-bearing twice over. This Dockerfile COPYs nothing, so everything
// outside this directory was only ever cost -- and devpod's prebuild tag
// is a hash *of the build context* as well as of the config
// (pkg/devcontainer/config/prebuild.go). With the root as context that
// hash moved on every commit to any file in the repository, so a
// prebuilt image could never match one.
//
// Scoping it here narrowed that to "every commit under
// `.devcontainer/**`", which is close to right but not the same claim as
// "every commit that changes the image", and the gap cost real launches.
// A Dockerfile with no COPY leaves devpod's includeFiles empty, and it
// then hashes the whole directory -- so this file's own comments, the
// feature's README, and the host-side `initializeCommand` were all
// hashed into the tag, none of which the image can see.
// `.dockerignore`, below, is what closes it; read that file for the
// measurement and the reasoning.
//
// The root `.dockerignore` is not read for this build -- docker looks
// for one inside the context, and there is one there now. The root
// file's patterns existed to keep `.pixi/` and `rust/target/` out of a
// root-sized context, and nothing of that shape is in here.
"context": ".",
// Best-effort layer cache for builders that know nothing about devpod
// prebuilds -- VS Code's "Reopen in Container", a plain
// `devcontainer up`. The prebuild declared under `customizations.devpod`
// is what actually skips the build for `dl`; this is the fallback for
// everything else, and a cache miss -- including a `:latest` that does
// not exist yet, as on the very first prebuild run -- is a warning
// rather than a failure.
"cacheFrom": "ghcr.io/blooop/devlaunch-devcontainer:latest"
},
"initializeCommand": ".devcontainer/claude-code/init-host.sh",
"customizations": {
// Where devpod looks for a prebuilt image before it builds anything.
// `devpod up` -- which is every `dl` launch -- computes the prebuild
// hash from the build config and context above, asks this repository for
// `<repo>:devpod-<hash>`, and uses that image directly if it is there,
// features and all. A matching prebuild turns a multi-minute build into
// a pull; a miss is logged at debug level and falls through to building
// locally, which is why nothing here has to be conditional or guarded.
//
// Declared here rather than passed as `--prebuild-repository` so that it
// applies to everyone who opens this repo with devpod without anyone
// having to remember a flag or an environment variable. `dl` passes no
// such flag and does not need to.
//
// `.github/workflows/devcontainer-prebuild.yml` is what fills it, on
// every push to `main` that touches `.devcontainer/**`. The package is
// public -- GHCR gave it the visibility of the public repository that
// published it -- so pulling needs no `docker login ghcr.io`. That is
// worth re-checking if launches ever get slow again rather than trusting
// forever: private, the lookup above comes back DENIED, which reads as a
// miss, and every launch silently builds locally exactly as it did
// before this existed.
"devpod": {
"prebuildRepository": "ghcr.io/blooop/devlaunch-devcontainer"
},
"vscode": {
"settings": {},
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"jjjermiah.pixi-vscode",
"charliermarsh.ruff",
"tamasfe.even-better-toml",
"mhutchie.git-graph"
]
}
},
"features": {
"./claude-code": {},
// A Docker daemon of our own, so devpod can create real containers from
// in here and the e2e suite (pixi run test-e2e) has something to run
// against. The feature is self-contained: it brings "privileged": true,
// its own entrypoint, and a named volume for /var/lib/docker -- which is
// what buys overlayfs rather than the vfs fallback, so nested builds run
// at normal speed. Nothing else has to be arranged for it.
//
// It declares no "init" key, and does not need one: PID 1 in here is
// devpod's agent, which reaps orphans already, and dockerd is one of
// them. Adding "init": true would stack a second init on a done job.
//
// This is also why the container does not join the host's network
// namespace. The host's own docker0 bridge is already up in there, so a
// second daemon would co-manage that bridge and write its NAT rules into
// the host's netfilter tables -- fighting the host's Docker rather than
// nesting under it -- and every port a process here listens on would be
// a port on the host. Nothing this repo reaches for needs host
// networking; a bridge network serves all of it.
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
// "ghcr.io/devcontainers/features/common-utils:2": {},
// "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {},
// "ghcr.io/jsburckhardt/devcontainer-features/codex:latest": {}
},
"containerEnv": {
"CLAUDE_CONFIG_DIR": "/home/vscode/.claude",
"XDG_CONFIG_HOME": "/home/vscode/.config",
"XDG_CACHE_HOME": "/home/vscode/.cache",
"XDG_DATA_HOME": "/home/vscode/.local/share",
"SSH_AUTH_SOCK": "/home/vscode/.ssh/agent.sock"
},
"mounts": [
"source=${localWorkspaceFolderBasename}-pixi,target=${containerWorkspaceFolder}/.pixi,type=volume",
// Two files, not the directory they sit in. devpod running in here
// writes `Host <id>.devpod` blocks into $HOME/.ssh/config, each with a
// ProxyCommand naming a devpod binary that exists only inside this
// container -- so mounting the directory put entries on the
// developer's real config that nothing outside could ever honour, and
// that survived the container they pointed at. Left container-local,
// they die with it.
//
// These two are what SSH actually needs here, measured rather than
// assumed: the agent socket alone authenticates to GitHub, so the
// private key is not load-bearing and is deliberately not mounted,
// while without known_hosts a clone fails host key verification.
// known_hosts is read-only because nothing in here has any business
// adding hosts to the developer's list.
//
// The source is a path the host-side hook maintains, and that is the
// third design here, because the first two each refused the create
// outright on some host. `${localEnv:HOME}/.ssh/agent.sock` as "an
// assumption ... but a pre-existing one" was false for every agent that
// is not a hand-rolled symlink -- gpg-agent answers under
// $XDG_RUNTIME_DIR, ssh-agent(1) under /tmp -- so a gpg-agent host got
// `bind mount source path does not exist`. `${localEnv:SSH_AUTH_SOCK}`
// fixed that host and broke the opposite one: devcontainer.json has no
// conditional mounts, an unset variable expands to nothing, and docker
// refuses the whole run with `field Source must not be empty`. That is
// a fresh desktop before any dotfiles have run, which is the first
// machine every new user has, opening the repo the README tells them
// to open first.
//
// Both designs made "no agent" mean "no container". What the mount
// needs is only that its source exist; whether an agent answers there
// is a separate question with a separate answer (dl says so on the
// terminal). So the source is the same stable path as before, and
// `claude-code/init-host.sh` -- the initializeCommand, run on the host
// before every create -- is what makes it true rather than assumed:
// a symlink to $SSH_AUTH_SOCK when that names a live socket, untouched
// when something has bound a real socket here already, an empty
// placeholder when the host has no agent at all. The hook's own comment
// has the three arms and what each protects.
//
// containerEnv hardcodes the *target*, and that stays hardcoded. Inside
// the container SSH_AUTH_SOCK is /home/vscode/.ssh/agent.sock whatever
// the host called it, so the container end of the contract is
// unchanged by any of this.
//
// Still fixed at create: docker resolves the source on the host when it
// makes the container, so an agent that appears afterwards is not seen
// until `recreate`. The direct binding had the same limit.
"source=${localEnv:HOME}/.ssh/agent.sock,target=/home/vscode/.ssh/agent.sock,type=bind",
"source=${localEnv:HOME}/.ssh/known_hosts,target=/home/vscode/.ssh/known_hosts,type=bind,readonly",
"source=${localEnv:HOME}/.config/gh,target=/home/vscode/.config/gh,type=bind"
],
// Register the docker provider with the container's own devpod. A fresh
// container has an empty ~/.devpod, and devpod seeds nothing: `devpod up`
// there exits 1 with "no default provider found". Since dl's only
// subprocess is devpod, that is dl failing to start rather than degrading.
// `provider add` also makes it the default, so this is the whole bootstrap.
//
// What it costs: this is now a three-link && chain on the critical path of
// every container create, and the new link can fail. The task refuses to
// guess when `devpod provider list` cannot be read -- it exits 1 rather
// than treating an unreadable answer as "no providers, add one" -- so an
// unreadable listing now fails container creation. That is the intended
// trade. The alternative is a container that comes up looking fine and
// fails on the first dl command anyone runs in it, which is the failure
// this step exists to remove; failing at create is louder, immediate, and
// attributable. It is idempotent, so a rebuild after a fix is the remedy.
"postCreateCommand": "sudo chown vscode .pixi && pixi install --frozen && pixi run dev-add-docker"
}