Goal
Keep Claude Code's account/session login (~/.claude.json) alive across
devcontainer rebuilds, instead of having to re-authenticate every time the
image is rebuilt.
The problem
Claude Code keeps two things on disk:
-
~/.claude/— a directory, already persisted via a named Docker volume (claude-playwright-setup). -
~/.claude.json— a single file holding account/session state, which was not persisted. Every container rebuild wiped it, forcing a fresh login.
Normally you'd just mount a named volume onto the whole folder the state
lives in, the same way .claude/, .copilot/, and .continue/ are already
handled. That's not an option here: .claude.json isn't inside its own
subfolder, it sits directly in $HOME alongside everything else
(.bashrc, .ssh/, .profile, ...). Mounting a volume onto $HOME itself
to catch one file would shadow all of that, so the file has to be persisted
on its own.
Mounting a named volume straight onto the file path
(claude-json-...:/home/container-user/.claude.json) seems like the
next-simplest option, but it breaks on this Docker Desktop setup:
mount ... not a directory: Are you trying to mount a directory onto a file
A named volume's backing store is always a directory. Docker is supposed to
detect that the mount target is a single file and copy the image's file into
the volume so it ends up binding file-to-file. On this Docker Desktop that
detection fails — the volume comes up as an empty directory, and runc then
tries to bind that directory onto the file path and crashes at container
start. This was confirmed by deleting the volume and rebuilding the image
from scratch, so it isn't a stale-cache artifact.
The fix
Never mount a volume directly onto a single file. Instead, mount it onto a
directory — the same shape already used for .claude/.copilot/.continue
— and symlink the dotfile into that directory from the Dockerfile.
Dockerfile.debian:
USER container-user
....
RUN mkdir -p /home/container-user/.claude-json && \
touch /home/container-user/.claude-json/claude.json && \
ln -s /home/container-user/.claude-json/claude.json /home/container-user/.claude.json
docker-compose.yml:
services:
devcontainer:
volumes:
- claude-json-playwright-setup:/home/container-user/.claude-json
volumes:
claude-json-playwright-setup:
name: setup-playwright-playground-debian_claude-json-playwright-setup
post-container-install.sh — fix ownership on the new mount, same as the
existing volumes (mounted volumes come up owned by root):
sudo chown -R container-user:container-user /home/container-user/.claude \
/home/container-user/.claude-json \
/home/container-user/.copilot \
...
.claude/settings.json and .claude/settings.local.json also gained a few
permission entries (docker volume *, docker run *, docker compose *,
docker history *, and a find ... -iname *.copilot*) needed to diagnose
and rebuild the container while working through this.
Caveat: atomic writes
If an app ever persists the file via write-temp-then-rename (an atomic
write) instead of writing in place, the rename replaces the symlink with a
plain file inside the container's ephemeral layer. No crash — but
persistence silently stops working until the next image build recreates the
symlink. Worth checking if login state ever mysteriously stops surviving a
rebuild again.
Top comments (0)