Skip to content

Container Safety

Source: hooks/ways/softwaredev/environment/container-safety/container-safety.md

Frontmatter
Field Value
description developer safety when a build or task runs inside a container — non-root execution, host artifact ownership, least privilege, scoped bind mounts
vocabulary container docker podman buildah nerdctl dockerfile containerfile compose image build stage bind mount volume rootless non-root user uid gid privileged capability drop artifact ownership blast radius developer safety
commands (docker|podman|nerdctl|buildah)\ (build|run|compose)
files Dockerfile|Containerfile|(docker-)?compose.ya?ml|.dockerignore|.devcontainer
refire 0.15
scope agent, subagent

When our build or a dev task runs inside a container — Docker, Podman, Buildah, Compose, devcontainers — the definition is a developer-safety surface, not just a build recipe. The convenient default (everything as root) quietly widens the blast radius of ordinary mistakes on our own machine. This isn't about a remote exploit; it's about not building the setup where a routine cleanup command can do real damage.

The failure mode we're avoiding: a build runs as root, writes root-owned artifacts into a bind-mounted host path, cleaning them up then needs sudo — and sudo in our muscle memory around build output is one slice of Swiss cheese away from an rm -rf landing on the wrong target. Least privilege in the definition keeps ordinary mistakes ordinary.

What we check in a container definition

Concern What to do Why
Who runs Set a non-root USER for the build/run stage Root in the container is root on any bind-mounted host path
Who owns artifacts Match the container uid/gid to the host user — ARG UID/GID, or run with --user $(id -u):$(id -g) Root-owned build output forces sudo to clean, escalating blast radius
How much privilege No --privileged, drop capabilities you don't need, don't bind-mount the docker socket into a build A build almost never needs host-level power; grant the minimum
What's mounted Mount the narrowest path, read-only where possible Never mount $HOME or / into a build — scope the surface
What ships Multi-stage: build in one stage, copy artifacts into a slim non-root runtime stage Keeps root and build tooling out of the final image

The shape we prefer

# build args let the image match the invoking host user
ARG UID=1000
ARG GID=1000

FROM builder AS build
# ... compile ...

FROM runtime AS final
RUN groupadd -g ${GID} app && useradd -u ${UID} -g ${GID} -m app
USER app                     # non-root from here on
COPY --from=build --chown=app:app /out /app

For a throwaway local build, the lighter move is to skip the in-image user and just run as the host identity: docker run --user "$(id -u):$(id -g)" -v "$PWD/out:/out:rw" … — artifacts land owned by you, sudo-free to clean.

See Also

  • environment/makefile(softwaredev) — the build task runner these commands usually sit behind
  • code/security(softwaredev) — least privilege as a general code concern
  • architecture/threat-modeling(softwaredev) — blast radius and the Swiss-cheese framing this leans on