Field notes · devcontainers
Colima & Podman: DevContainer Engine Support
DevContainers became fundamental to the setup that enables our AI-native SDLC. We didn't want to depend on Docker Desktop, which requires a paid licence, but we also didn't want to drop Docker Desktop-compatible config in case a team that relies on it adopts our setup. So the devcontainer now supports three interchangeable engines, selected with a docker context switch.
Why
Nothing in docker-compose.yml, devcontainer.json, or the Dockerfile is engine-specific: they only talk to the Docker CLI and API, so the same devcontainer definition runs unmodified no matter which engine backs it. The three engines we support under .devcontainer/ are Docker Desktop, Colima, and Podman Desktop, and an engine is selected with a docker context switch.
Colima
Colima runs the same Docker engine in a lightweight Linux VM, free and open source. make colima-up (wrapping .devcontainer/colima.sh) starts a dedicated profile, so a developer's own default Colima VM is never resized, then switches the docker CLI context to it.
The profile pins four settings:
--runtime dockerwithfeatures.containerd-snapshotter: falsekeeps the overlay2 image store. Since v0.8.3 Colima writescontainerd-snapshotter: trueinto the VM'sdaemon.jsonby default, even with the docker runtime, and that store strips the setuid bit fromsudoin the image, breakinginit-firewall.shand the post-create lifecycle. Pinned on every start.--vm-type vz --vz-rosettaon Apple Silicon. Rosetta runs the x86_64-only Android tooling (adb,aapt2) inside the arm64 image.--mount-type virtiofs --mount ~:wbecausedocker-compose.ymlbind-mounts the repo and several paths under~, some writable.--memory 8 --cpus 4 --disk 100because Colima's default of 2 GiB isn't enough for the Flutter and Gradle build plus LocalStack and the Lambda tests. Overridable per run:COLIMA_MEMORY=12 COLIMA_CPUS=6 make colima-up.
The dual-socket symlink. docker context covers the CLI, VS Code, and the devcontainer CLI, but the JetBrains Docker plugin ignores it and hardcodes a fixed connection path instead. make colima-up therefore also symlinks two paths straight at Colima's socket:
/var/run/docker.sock: the classic system socket (root-owned, asks for a password).~/.docker/run/docker.sock: Docker Desktop's per-user socket. Since Desktop 4.13 it no longer creates/var/run/docker.sockby default, and this per-user path is the one IntelliJ'sremote-servers.xmlhardcodes as its default connection (apiUrl="unix://$USER_HOME$/.docker/run/docker.sock"). It doesn't fall back to the system socket.
make colima-down restores whichever engine each symlink pointed at before (Docker Desktop's, if it was there), or removes it if there was nothing.
Podman Desktop
A SELinux compose flag. Podman's machine VM runs SELinux in Enforcing mode. That denies the /var/run/docker.sock bind-mount the LocalStack service needs to spawn Lambda containers, failing with "Error while creating lambda: Docker not available". Fixed with a compose-level flag:
security_opt:
- label=disable
label=disable is a documented no-op on engines with no SELinux (Docker Desktop, Colima), so it's safe to leave on unconditionally. The standalone LocalStack path (localstack-deploy.sh) appends the matching flag, keeping any flags the developer already had: DOCKER_FLAGS="${DOCKER_FLAGS:-} --security-opt label=disable".
How the switch actually works
All three engines expose the same Docker API over a Unix socket, and docker context is just a named pointer to one of those sockets. make colima-up and make colima-down (or picking Podman Desktop's context) only change which socket the docker CLI, VS Code, and the devcontainer CLI currently point at. The compose file, devcontainer.json, and the Dockerfile never know or care which engine answers.
The one cross-engine constant every option needed regardless: disable the containerd image store, wherever the engine puts that toggle (Docker Desktop's settings, OrbStack's, Rancher Desktop's, or Colima's containerd-snapshotter: false pin). The containerd store strips the setuid bit during image extraction, so sudo inside the container loses its s bit and its root ownership, breaking init-firewall.sh and the whole post-create lifecycle. That single incompatibility is why this needed a documented per-engine setting at all, rather than being invisible.
A credential-store pitfall
Docker Desktop configures "credsStore": "desktop" in ~/.docker/config.json, and the setting outlives the app. If docker and docker-compose were installed from Homebrew, the config still references the docker-credential-desktop helper, a binary that exists only inside Desktop's bundle. Homebrew has no formula for it, and the /usr/local/bin/docker-credential-desktop symlink Desktop leaves behind points into the deleted Docker.app. The Docker CLI asks the helper for credentials before every registry request, even for a public, anonymous image, so docker pull hello-world fails with:
error getting credentials - err: exec: "docker-credential-desktop": executable file not found in $PATH
and the devcontainer's docker compose build fails with the matching error listing credentials message.
To fix it: brew install docker-credential-helper, then set credsStore to osxkeychain, the store Docker uses by default on macOS without Desktop. The preflight in colima.sh now warns on this exact mismatch (credsStore: desktop with no matching binary on PATH) on every make colima-up.