Dev Container
Artifact Depot ships two dev container configurations. Both build from the
single root Dockerfile
— the same image CI and the release build use — they just target different
build stages. In VS Code, Dev Containers: Reopen in Container shows a picker
when more than one config is present; pick the one you want.
| artifact-depot (default) | artifact-depot (dev-non-root) | |
|---|---|---|
| Config | .devcontainer/devcontainer.json |
.devcontainer/dev-non-root/devcontainer.json |
| Build stage | dev |
dev-non-root |
| Runs as | root |
your host user (UID-matched) |
| Setup needed | none | a few host-side files (below) |
Both share the same named volumes (/workspace, /worktrees, /build), bind
the repo at /workspace/artifact-depot, run privileged (so the make
end-to-end tests that start dockerd work), and forward ports 4000 (docs) and
8080 (depot).
artifact-depot (default / community)
The recommended starting point. Just “Reopen in Container” — no setup. It contains the complete build/test toolchain plus the conveniences most contributors want:
- Toolchain (from the shared
builderstage): Rust 1.94.1, Node 24, the Playwright system libraries, DynamoDB Local,cargo-deny/cargo-about,reuse,mold— everythingmake(lint + build + test + e2e) needs. - Conveniences (the
devstage):gh,git,vim,less,ripgrep,jq,shellcheck,hadolint,skopeo,helm,docker-compose, Ruby + Jekyll (docs site), and theclaude/codexCLIs. - Extensions: rust-analyzer, GitLens, shellcheck, hadolint, YAML, Volar, Claude Code.
It runs as root. The trade-off: files you create in the bind-mounted repo are owned by root on your host. If that bothers you, use the dev-non-root container.
artifact-depot (dev-non-root)
Everything in the default container, plus a non-root user matched to your
host UID (so bind-mounted files keep your ownership), glab and the aws
CLI, a real in-container Docker daemon, access to a few of your host config
folders, an env-file bridge, and an optional Beyond Compare install.
Because it runs its own in-container dockerd (like CI and the default
container), the docker-based integration tests run here — test-docker-auth
and test-apt launch containers against a depot on the dev container’s
localhost. The Docker engine and the buildx plugin are baked into the image;
the daemon itself is started on every container launch by a postStartCommand
(.devcontainer/start-dockerd.sh),
and /var/lib/docker is a named volume so pulled images survive rebuilds. The
dev-non-root stage bakes /etc/docker/daemon.json marking the loopback registry
insecure, matching CI’s dockerd --insecure-registry 127.0.0.0/8.
What passes here. The full make test suite — lint, build, cargo test,
and the test-ui, test-dynamodb, and test-docker-auth integration suites —
passes non-root in this container, identically to root/CI. (test-apt is a
separate scripts/ext-test.sh apt mode, not part of make test, and is
maintained elsewhere.)
test-ui and test-dynamodb give each server a free ephemeral port on the
host loopback (see
scripts/net-helpers.sh),
which keeps parallel suites/worktrees from colliding with no privilege. They
previously used a network namespace for that isolation — which did work
non-root (via sudo ip netns, since this container grants passwordless sudo),
but it required privilege and leaked namespaces/processes when a run was
interrupted; free ports need neither.
test-docker-auth is the one suite with irreducibly privileged steps: it starts
a root-owned containerd and dockerd and installs a self-signed CA into the
system trust store. Those specific steps run via the container’s passwordless
sudo (see the as_root helper in net-helpers.sh), while ctr and docker
themselves run as the dev user via the docker group. This is exactly why the
dev-non-root container is privileged with NOPASSWD sudo — not a workaround,
but what lets the daemon-based test run the same way whether root or non-root.
Select it with Dev Containers: Reopen in Container → “artifact-depot (dev-non-root)”.
Host files it uses
initializeCommand creates these on your host if missing (empty), then binds
them in, so the container starts even if you have none of them:
~/.ssh(read-only) — git push / ssh to remote hosts~/.claude— Claude session history persists across rebuilds~/.aws— AWS credentials for the S3 / DynamoDB backends~/.artifact-depot.bash_history— shell history persists across rebuilds
~/.dev_aliases — your env + aliases
Create ~/.dev_aliases on your host and export whatever you want available
inside the container:
# ~/.dev_aliases
export TZ=America/Chicago
export GITLAB_HOST=gitlab.example.com # if you use glab
# export BC5_LICENSE_KEY_B64=... # see Beyond Compare below
How it works: initializeCommand snapshots the variables this file exports
into ~/.artifact-depot.devenv, and runArgs: --env-file imports them into the
container — so they reach non-interactive shells too (Claude Code’s tool
shell, scripts), not just login shells. The same file is also mounted as
~/.bash_aliases, so interactive shells pick up any aliases/functions.
Beyond Compare 5 (optional)
Set one variable to opt in; leave it unset and nothing happens.
- Save your BC5 license key block (the multi-line text from Scooter) to a
file, e.g.
~/bc5key.txt. - Base64-encode it into
~/.dev_aliases:# Linux echo "export BC5_LICENSE_KEY_B64=$(base64 -w0 ~/bc5key.txt)" >> ~/.dev_aliases # macOS echo "export BC5_LICENSE_KEY_B64=$(base64 -i ~/bc5key.txt | tr -d '\n')" >> ~/.dev_aliases(Base64 keeps the multi-line key on one line for the env-file and dodges quoting issues. Never commit your key.)
- Rebuild/reopen the dev-non-root container. The
beyond-comparepostCreateCommandruns.devcontainer/install-bcompare.sh, which installs Beyond Compare 5 and licenses it (writes/etc/BC5Key.txt).git difftool/git mergetooluse it viadiff.tool=bc/merge.tool=bcfrom your host~/.gitconfig(copied into the container by VS Code).
The GUI (git difftool) needs a forwarded display. On Linux, VS Code
forwards a Wayland socket automatically and the wrapper falls back to it. For
the crisp X11 (xcb) path, authorize the host X server for local clients once
with xhost +local: on the host — persist it via a GNOME autostart entry on
Wayland (~/.config/autostart/), or ~/.xprofile on an X11 session. Otherwise
it uses Wayland. The install + license itself works with no display.
Under the hood — the Dockerfile stages
node ─┐
▼
builder ──► dev ──► dev-non-root (CI: --target builder)
│ (default container: dev)
▼ (dev-non-root container: dev-non-root)
release-build ──► runtime (release image; default build target)
One Dockerfile, one place that pins Node/Rust. dev is FROM builder,
dev-non-root is FROM dev, so each layer inherits the one before it. CI
targets builder; the release image is runtime. The dev tooling never
reaches CI or the shipped image because those target different stages.
Notes
- Git worktrees: create them under
/worktrees(a named volume that persists across rebuilds), or anywhere under/workspace(also a volume). - The
/workspacevolume keeps sibling clones / scratch dirs across rebuilds; your checkout is bind-mounted into it at/workspace/artifact-depot.