Repository navigation
Shared pixi cache mount makes ~/.cache root-owned in any image that lacks it, and hard-fails on a uid mismatch #240
Description
The shared pixi cache mount (#232, PR #236) targets /home/vscode/.cache/devlaunch-pixi. When the image has no /home/vscode/.cache — which the stock devcontainer images do not — Docker creates the missing parent as root:root, and the container user loses ~/.cache entirely.
Measured (2026-08-16, docker 29.7.2)
Stock mcr.microsoft.com/devcontainers/base:ubuntu, run as uid 1000 (vscode):
~/.cache owner |
mkdir ~/.cache/pip-test |
|
|---|---|---|
| no mount (control) | 1000 |
OK |
| with the mount as shipped | 0 (root) |
Permission denied |
The pixi mount itself stays writable, so pixi global sync is fine — everything else that writes ~/.cache (pip, uv, pre-commit, fontconfig…) silently breaks.
Which images are hit — it depends only on whether the image already ships ~/.cache:
| image | ships ~/.cache |
with mount |
|---|---|---|
devcontainers/base:ubuntu, base:ubuntu-24.04, rust:latest |
no | BROKEN |
wayfinder-devcontainer (built), devlaunch-main (built) |
yes (uid 1000) | writable |
So our own built images are unaffected, and every stock third-party devcontainer is. Mount args are appended unconditionally (dl.py:3656), so no dotfiles are involved; it applies to newly created containers only, since mounts are fixed at create time.
Second, related failure: cross-uid writes
PIXI_CACHE_DIR pointing at a directory the container user cannot write is a hard failure, not a degradation — pixi does not fall back to reading. Measured with a warm cache already containing the package:
× failed to open '/shared/repodata/....shards-cache-v1'
╰─▶ Permission denied (os error 13)
Error: × Couldn't install the following environments: jq (exit 1)
Control: the same uid with its own writable cache installs fine, so the cause is the cache's ownership, not the uid. Sharing therefore requires container uid == host uid, or root. Every mainstream base declares remoteUser = vscode/node at uid 1000, and this host is uid 1000, so the common case is safe; an image with any other uid fails to provision tools at all.
Making the host dir 0777 does rescue the cross-uid case in a simple test, but it is not a rigorous fix: package subdirectories are created 0755 owned by whichever uid wrote them, so two uids can still block each other on a shared package or repodata file.
Third: the dotfiles chown amplifier
blooop/dotfiles install.sh:44-48 chowns -R "$USER:$USER" "$HOME/.cache" when it finds it root-owned — and the mount manufactures exactly that precondition. Verified write-through: a container at uid 2000 chowned the host directory 1000 → 2000, after which the host user could not write its own cache. A second container at uid 1000 chowns it back — the two ping-pong on every launch. (install.sh:131-133 reaches it by a second path, guarded on .config writability but chowning .cache too.)
Done when
The mount target moves out of $HOME — e.g. /var/cache/devlaunch/pixi, whose parent exists in every image — which fixes all three at once: no root-owned ~/.cache, no dependence on the container's home path or username, and out of reach of the dotfiles chown. Pin it with a test asserting the target is not under any home directory, and add a preflight exists() check on the mount source (a typo'd source currently fails up loudly, then a retried ssh silently starts a container without the mount). Decide and record what happens when the container uid cannot write the cache — at minimum it should not be a hard provisioning failure.
This ticket was generated by AI during investigation follow-through; all measurements above were taken live on 2026-08-16.
Activity
blooop commentedon Aug 16, 2026
breadcrumb: Building this as: move the shared pixi cache mount target from
/home/vscode/.cache/devlaunch-pixito/var/cache/devlaunch/pixi, so the mount can no longer manufacture a root-owned~/.cachein an image that ships none.Seams I will test at, all pre-agreed by the ticket:
upargv builder for the pixi cache — the existing shared-pixi-cache suite, which already pins mount/workspace-env/dotfiles-script-env spellings. New test there asserting the invariant the regression violated: the target is under no home directory (/home/...,/root/...). Red first.upcommand entire and so has to move in step.Verified on the real image before writing code:
/var/cacheexists in stockdevcontainers/base:ubuntu(root:root, FHS-standard), the mounted dir is writable by uid 1000, and~/.cachestays user-owned — where the shipped target leaves it root-owned and unwritable, reproducing the ticket's table exactly.blooop commentedon Aug 16, 2026
Lifecycle behaviour, measured live (devpod 0.26.1 / docker 29.7.2, isolated
DEVPOD_HOME). These decide what the fix has to document about existing workspaces.upwith the mountstopthenup(mount not re-passed)up --recreatewithout re-passing--mountup --recreatewith--mountup --mount …on an existing containerdocker run)Two consequences:
--mountinto workspace config. It must be re-passed on everyup, which devlaunch does — but it also means a mount can only ever be added by a recreate. A devlaunch upgrade therefore does not reach existing workspaces: they keep running without the cache, silently, untildl <ws> recreate.~/.cachebreakage heals on recreate but not on stop/start./home/vscodesits on the container's overlay layer, sostop+upkeeps the root-owned directory, while--recreategets a fresh layer where.cacheis recreatedvscode-owned. So the remedy for anyone already affected isdl <ws> recreate, and that is worth a README line.Also confirmed about the mechanism itself: the root-owned
~/.cachearises from any root-context command that touches$HOMEbefore the user does — the mount is the reliable trigger, not the only one.Caveat, untested: a devcontainer that mounts the user's home as a named volume would not heal on recreate, because home would no longer be on the disposable layer. Worth one follow-up check before promising the remedy unconditionally.
blooop commentedon Aug 16, 2026
breadcrumb: PR #241, green.
The mount target is now
/var/cache/devlaunch/pixi— FHS-required parent, present in every base this launches, outside every home. Verified on the real stock image before writing code: with the shipped target~/.cachecomes out root-owned and unwritable; with the new one it stays the user's own and the mounted dir is writable. Moving out of$HOMEalso drops the hardcoded remote-user guess and puts the cache out of reach of dotfiles installs that chown a root-owned home cache through the bind onto the host.Pinned red-first by the invariant the regression violated — the target is under no home directory, stated over homes rather than over the one path that regressed — plus a parent-exists assertion and a preflight that emits the mount args only when the source really exists. Existing argv and whole-argv spawn pins moved in step.
The uid question is recorded as a documented constraint rather than a fix: pixi hard-fails on a cache it cannot write instead of degrading to reads, and devlaunch cannot see a container's uid before launching it, so there is nothing to decide from the outside. The alternatives considered and rejected are written up in the PR body rather than built. Common case is safe; README states the limit and the recovery.
One thing a reviewer should weigh: this regression is on main only — the feature that introduced it has not been released — so the CHANGELOG carries both a
Fixedentry and a corrected path in the originalAddedentry, which sit in the same Unreleased block.blooop commentedon Aug 16, 2026
Review of PR #241: Standards Approve, Spec Request changes — and the blocking finding was real, reproduced independently before acting.
Moving the target to
/var/cache/devlaunch/pixifixed the~/.cacheregression but broke the migration seam: devpod re-applies--workspace-envon everyupwhile mounts only land at creation, so every pre-existing workspace would getPIXI_CACHE_DIRpointing at an unmounted, root-owned path —pixi global installexit 1, where today's target merely fails to share. That is exactly what this ticket's "at minimum it should not be a hard provisioning failure" forbids.Fixed on the branch by targeting
/var/tmp/devlaunch-pixiinstead:/var/tmpisdrwxrwxrwtin stock images, so an unmounted target stays writable (measured exit 0), it is still outside$HOME, and because it pre-exists nothing intermediate is created as root. Re-review before merge. #241Land the shared pixi cache on /var/tmp, not /var/cache (#240)