01 — Containerization (Docker)
Focus: Docker. How BitVault’s deployables become small, secure, reproducible, multi-arch images — the artifact every later stage promotes. Image build/sign/scan pipelines are in 07; this doc is the image design.
1. What we containerize
Per ADR-0001 the system is a modular monolith with extractable workers, so the image set is small:
| Image | Contents | Base |
|---|---|---|
bitvaultd |
the control-plane binary (all modules in v1) | distroless static / scratch |
bitvault-worker |
async workers (indexer, notifier, GC, scrubber, packer) — first extraction | distroless static / scratch |
bitvault-web |
Next.js server (SSR) | distroless nodejs |
| (CLI) | shipped as a signed static binary (+ optional image), not a deploy artifact | — |
One Go codebase → multiple binaries → multiple tiny images; the worker image is the same code with a different entrypoint until it is truly extracted.
2. Image design rules
- Multi-stage builds. A fat builder stage (Go toolchain / pnpm) produces the artifact; the final stage copies only the binary + runtime files. The toolchain never ships.
- Minimal, non-root, hardened final image. Go services → distroless static or
scratch(CGO disabled → a single static binary, no shell, no package manager). Run as a non-root UID, read-only root filesystem, all Linux capabilities dropped,seccomp: RuntimeDefault. This satisfies the PodSecurity restricted profile (02). - Reproducible. Base images pinned by digest (not floating tags); deps pinned
(
go.sum,pnpm-lock.yaml); BuildKit for deterministic, cacheable builds. - Multi-arch.
linux/amd64+linux/arm64viabuildx(Graviton/Ampere nodes are cheaper; dev laptops are arm64). - No secrets in layers. Never
ARG/ENVa secret; use BuildKit secret mounts for build-time creds;.dockerignorekeeps.git, env files, and local junk out. - Layer ordering for cache. Dependency fetch (
go mod download/pnpm install) before source copy, so source edits don’t bust the dependency layer. - No in-image healthcheck. Liveness/readiness are Kubernetes probes
(
/healthz,/readyz, ADR-0013), not DockerHEALTHCHECK— K8s owns lifecycle.
3. Tagging & identity
- Deploy by immutable digest (
@sha256:…) — the only thing referenced in the GitOps repo (06), so a tag can never be silently moved under a running environment. - Also publish human tags: semver (
v1.4.2) for releases and git SHA (sha-abc1234) for traceability (08). - The digest is the promotion unit dev→staging→prod (ADR-0034) — build once, promote the same bytes.
4. Supply-chain properties (built in 07)
Every published image carries: an SBOM (syft), a passing vulnerability scan (trivy/grype), a keyless cosign signature (sigstore/OIDC), and SLSA provenance. Clusters verify the signature at admission before running it (ADR-0032). The image is not just small — it is attestable.
5. Self-host parity
The same images run under Docker Compose for self-host (ADR-0012); distroless + static binaries make the self-host footprint tiny and dependency-free.
6. Tradeoffs / Alternatives / Scaling
Tradeoffs. Distroless/scratch images are tiny and have a minimal attack surface but
have no shell → no kubectl exec debugging. Mitigation: ephemeral debug containers
(kubectl debug) attach a toolbox without bloating the image; structured logs + traces
(ADR-0013) reduce the need to shell in.
Alternatives considered.
- Alpine base: small and has a shell (easier debugging) but adds a package manager + musl quirks + more CVEs to track. Rejected as default; distroless wins on attack surface. Alpine acceptable for the optional CLI image.
- Ubuntu/Debian slim: familiar, but large and CVE-heavy. Rejected for runtime images.
- Buildpacks / ko (for Go):
kois attractive for Go (no Dockerfile, reproducible, SBOM-friendly) — a viable build backend for the Go images; the image design here (distroless, non-root, multi-arch, signed) holds regardless of builder.
Scaling concerns.
- Registry pull pressure at scale → multi-arch + small images reduce pull time; registry pull-through cache / CDN; pin digests to leverage layer caching.
- Build time in CI → BuildKit layer cache + remote cache; path-filtered builds so a web change doesn’t rebuild Go images (07).
- CVE churn → automated base-image digest bumps (Renovate/Dependabot) + scan gates; rebuild-on-base-update so images don’t rot.
References
- Distroless: https://github.com/GoogleContainerTools/distroless
- Docker BuildKit secrets & multi-arch: https://docs.docker.com/build/
- PodSecurity restricted profile: https://kubernetes.io/docs/concepts/security/pod-security-standards/