The container image build policy decision record: why the image build model you chose determines your vulnerability surface and your build time ceiling

Published 2026-07-13 · WhyChose

Container image decisions are made in the first containerization session when the team needs to package the application for consistent deployment across environments. The AI session that produces the container image decision is practical and results-oriented: it evaluates whether to use Docker, Podman, or another OCI-compatible build tool; selects a base image that will run the application's language runtime; writes a Dockerfile that installs dependencies, copies the application source, and defines the container entrypoint; and produces a working image that can be run locally and deployed to the team's container runtime. The session confirms that the application starts correctly in the container, that the entrypoint command matches the local development start command, and that environment variables required for the application are documented. The Dockerfile is checked into version control. The decision is correct and the outcome is what the team needed: the application runs in a container, the container can be pushed to a registry, and the deployment pipeline has a consistent artifact to promote from development to staging to production.

What the AI session does not produce is the container image build system's second half. The session answers "how do we containerize this application?" and produces a working Dockerfile. It does not ask: what is the base image selection policy — which OS family, which runtime variant (full, slim, alpine, distroless), and which criteria determine which variant is appropriate for a service, and is there an approved image list that code review can verify new Dockerfiles against? What is the layer ordering requirement for cache effectiveness, and what build time budget applies to each image type so that the dependency install layer is not invalidated by source code changes on every build run? What is the vulnerability scan policy — which severity levels block a PR merge, what is the rebuild cadence when a critical CVE is disclosed in an upstream base image package, and what is the exception process for CVEs without an available fix? What is the image signing and SBOM policy, and what secrets handling requirement prevents build-time credentials from being baked into the image layer history? Each of these questions has an answer that is not derivable from "we use Docker with node:18" as the team frames the containerization choice. Each answer determines whether the container fleet's vulnerability surface grows silently across twelve services over twenty-two months without a scan integration to detect it, or whether a base image variant mismatch between two services produces a systematic calculation error that passes all tests and surfaces through a customer escalation four months after deployment. The answers exist in the AI sessions. They are the operational commitments behind the image build model choice. They are almost never written down.

Two ways container image decisions produce the wrong outcome at scale

No base image policy and the CVE accumulation problem

A developer tools startup builds its first containerized service using node:18 as the base image — not the alpine or slim variant, because the developer writing the Dockerfile is focused on getting the service to run, not on image size or vulnerability surface. The base image choice is the path of least resistance: node:18 is the first result in Docker Hub's Node.js repository, runs without compatibility issues for any Node.js application, and includes curl, gcc, make, openssl, and several other system packages that are useful for debugging and compilation. At founding time the image is 892 MB, the first build takes 42 seconds, and the service deploys correctly to the team's Kubernetes cluster. The Dockerfile is practical and reasonable for a team of three trying to ship a product.

Over twenty-two months, the team grows from three to nineteen engineers and adds eleven more containerized services. Each service is written by a different engineer, each engineer uses the base image they are most familiar with, and there is no base image selection document to check against. The resulting fleet after twenty-two months uses: node:18 (three services), node:20 (two services), node:18-bullseye (one service built by an engineer who knew about slim variants but chose the full Bullseye image for compatibility certainty), python:3.11 (two services), python:3.11-slim (one service), ubuntu:22.04 with Node.js installed via apt (one service, written by an engineer who needed apt for a build dependency and defaulted to Ubuntu as the base), and node:20-alpine (one service, written by an engineer who had read about alpine's security advantages). No base image selection criteria are documented. No image size budget is enforced. No vulnerability scanner is integrated into any CI pipeline. No one has a complete inventory of which base images the fleet uses because the information exists only in twelve separate Dockerfiles.

In month twenty-three, a prospective enterprise customer's security questionnaire asks the startup to provide evidence of container vulnerability scanning and CVE remediation SLAs. The team configures Trivy to scan all twelve production images currently in the registry. The scan produces results across the fleet: 847 unique CVEs, of which 312 are rated CRITICAL or HIGH severity. The distribution is illuminating: forty-seven of the CRITICAL or HIGH CVEs exist in packages installed by the full node:18 Debian base image (curl, gcc, make, perl, openssl 1.1.x, python3) that are not used by any of the three Node.js services that use that base — they are present in the base image because the Debian base includes a general-purpose toolchain, not because any service depends on them. The largest image in the fleet, belonging to the ubuntu:22.04-based service, has grown to 3.2 GB: the engineer who built it installed gcc, cmake, and libssl-dev as build dependencies for a native addon, ran apt-get install in the same Dockerfile stage that produces the runtime image, and never separated the build environment from the runtime environment in a multi-stage Dockerfile. The native addon is 2.4 MB compiled; the build tools required to compile it add 1.1 GB to the runtime image. The build pipeline, running in the team's CI environment (GitHub Actions) without a layer cache, pulls the full base image and re-runs npm install on every build: the node:18 image is pulled fresh on each run at 927 MB, and the combined pull-and-install time for the three services using node:18 averages 8–14 minutes per build. The developer experience for a one-line application code change is a twelve-minute CI wait.

The remediation audit takes six weeks. The primary work: switching all services from full base images to either slim or alpine variants based on native addon compatibility requirements, reducing the twelve images from an average of 720 MB to an average of 145 MB; implementing multi-stage Dockerfiles for the four services that require build-time compilation tools so that the runtime image contains only the compiled output and the runtime packages; configuring Docker layer caching in the GitHub Actions pipeline using the actions/cache action with Docker's buildx cache; and integrating Trivy into each service's CI pipeline with a policy of failing builds on CRITICAL and HIGH CVEs that have a fixed version available. The remediation also requires documenting the base image selection policy for the first time, in a decision record that the team writes during the audit to prevent the same divergence in the next twelve services. The six weeks of remediation work was not in any roadmap quarter. The twelve-minute CI builds and the 847 CVE disclosure to the prospective enterprise customer preceded it by twenty-two months of building without a policy that the founding containerization session could have produced in the same session that wrote the first Dockerfile.

Inconsistent base image variant and the silent behavior regression

A B2B analytics platform decides in month three of development to use node:20-alpine as the standard base image for all Node.js services. The rationale is correct: alpine images are smaller than full Debian images, have a lower CVE surface because musl libc and BusyBox have fewer disclosed vulnerabilities than the equivalent Debian package set, and are widely used in production Node.js deployments. The decision is communicated as a Slack message in the team's #engineering channel. The message is direct: "use alpine for all Node containers going forward — smaller, more secure, less attack surface." All engineers who are in the channel on that day read it. The decision is never written in a document that new engineers would find during onboarding, and it is not enforced in CI. The channel has 1,247 messages by month eighteen, and the alpine requirement is not pinned or searchable.

At founding time, no service on the platform uses a native addon sensitive to the C standard library implementation. Node.js's own JavaScript runtime, V8, and the standard library modules (fs, http, net, crypto) behave identically on musl libc and glibc for the workloads the platform runs. Alpine is an invisible choice in the sense that matters most to an engineer evaluating it: the application works correctly, the tests pass, and no customer-visible difference appears. This invisibility is the source of the problem. Because the alpine/musl choice produces no observable difference at the time it is made, there is no signal to tell a future engineer that the choice was a constraint rather than a preference — that it was a decision requiring an approved base image policy document that new engineers could verify against, rather than a Slack message that recedes into channel history.

In month eighteen, the platform adds a financial calculation service that performs exact decimal arithmetic on revenue figures. Floating-point arithmetic produces rounding errors that accumulate in financial calculations — a sum of one million 0.001 values may produce 999.9999999999... rather than 1000.0 depending on the floating-point implementation details. The engineering team evaluates native addons for exact decimal arithmetic and selects one that implements IEEE 754 decimal arithmetic natively. The addon's installation documentation and its README both note that it "requires a glibc-compatible environment for correct operation" and "has been tested on Debian 11 and Ubuntu 22.04." The engineer who builds the financial calculation service reads the addon's documentation and chooses node:20-slim as the base image: a Debian Bookworm-based image with glibc, without the full toolchain, matching the addon's documented test environment. This engineer joined the team in month fourteen — four months after the alpine Slack message — has never seen the message, and has no document to check. The Dockerfile is submitted for code review. Two senior engineers review it. Neither notices the base image variant difference from the rest of the fleet because neither has the fleet's base image inventory memorized, and there is no CI lint step that checks FROM instructions against an approved list. The service is merged and deployed.

For four months, the financial calculation service produces correct output for all existing customers' datasets. The existing customers have transaction volumes that produce totals within a range where musl and glibc produce identical rounded results at the precision the platform reports. In month twenty-two, a new customer whose business model involves high-volume fractional transactions — millions of transactions per reporting period at values of $0.001 to $0.01 — imports their first dataset. The analytics pipeline (running on alpine/musl) calls the financial calculation service (running on bookworm-slim/glibc) through an internal API. The calculation service produces the correct decimal result under glibc's IEEE 754 implementation. The analytics service applies a final formatting and rounding step under musl's implementation. The two libc implementations differ in their handling of midpoint rounding for values with exactly five in the least-significant position being rounded: glibc uses round-half-to-even (banker's rounding) by default in some operations; musl's behavior in certain decimal-to-string conversion contexts produces round-half-away-from-zero. For the new customer's transaction distribution, which has a high proportion of values landing at the exact midpoint of the rounding range, the combined glibc-calculation-then-musl-formatting pipeline produces revenue figures that differ from both a pure-glibc result and a pure-musl result — it produces the third outcome available when two implementations with different rounding behaviors process the same value in sequence.

The customer reports to their account manager that the platform's revenue figures differ from their accounting system by 0.3–0.7% depending on the reporting period — too large to be expected floating-point noise, but small enough that no test caught it, because the test fixtures were not drawn from the customer's transaction distribution and the discrepancy is not constant. A three-day investigation involving senior engineers from both the analytics and financial calculation services traces the cause to the base image variant mismatch. The remediation decision — migrate all services to node:20-bookworm-slim to eliminate the musl/glibc boundary in the processing pipeline — requires updating twelve Dockerfiles, rebuilding and re-testing all twelve images, and deploying the updated fleet in coordination with the financial calculation service to ensure the musl rounding behavior disappears from the analytics path simultaneously. The migration takes eight days of engineering time and two coordinated deployment windows. The base image selection policy is written and entered into a decision record during the migration, specifying the approved base image list per language runtime, the native addon compatibility requirement that mandates glibc-based images, and the CI Dockerfile lint step that fails builds using non-approved base images. The policy that prevents the next variant mismatch is written after the incident that revealed the cost of not having it — not in the month-three session that established the alpine convention, when the engineering cost of writing it would have been three hours rather than eight days.

Three structural properties that container image decisions determine

The base image selection model and the vulnerability surface

The base image is not a neutral infrastructure detail — it is the largest single contributor to the container image's vulnerability surface and the primary determinant of runtime behavior compatibility for services that use native addons. The base image choice at the time of the first containerization session establishes a default that propagates across the fleet: every engineer who writes a new service Dockerfile looks at existing Dockerfiles to understand "what we use" and replicates the most common base image, with or without understanding the rationale behind the choice. An approved base image list with documented selection criteria converts this propagation from "replicate whatever was used before" to "select from the approved list according to the documented criteria."

The selection criteria for production runtime images organize along three axes. The first axis is the OS family and its libc implementation. Debian-based images (full, slim, bookworm, bullseye variants) use glibc — the GNU C Library — which is the most widely-tested libc implementation and the one against which the majority of native addon packages are built and tested. Alpine uses musl libc, which is smaller, has a lower CVE surface in the libc itself, and is the correct choice for services that do not use native addons or that have verified their native addons against musl. Distroless images (from Google's gcr.io/distroless project or Chainguard's cgr.dev registry) contain only the application runtime and its minimal dependencies without a shell, package manager, or system utilities. They have the lowest CVE surface because the largest categories of CVEs in slim images — coreutils, bash, find, grep, curl — are not present. The trade-off is debuggability: distroless images cannot be exec'd into for interactive troubleshooting. The correct policy for the OS family axis is: slim Debian is the default for services with native addon requirements or glibc compatibility constraints; alpine is the default for pure-language-runtime services without native addon dependencies; distroless is an option for security-hardened production services in mature projects where the team's debugging workflow does not rely on container exec.

The second axis is the multi-stage build requirement. Build-time dependencies — compilers, linkers, header packages, test frameworks, documentation generators — should not appear in the runtime image. A service that requires gcc to compile a native addon during npm install should use a build stage with a full or slim image containing gcc, compile the addon, and copy only the compiled output and the application source into the runtime stage using a minimal base image. The runtime image does not contain gcc because gcc was only needed to produce the addon binary. Multi-stage builds are the mechanism that enforces this separation, and the decision record must specify that the multi-stage build pattern is required for any service that installs build-time dependencies. A single-stage Dockerfile that runs apt-get install for build tools and then runs the application is not acceptable for production images regardless of the team's build time budget, because the build tools' CVE surface is carried into every production container instance for the lifetime of the service.

The third axis is the EOL and update cadence. Base images are tied to upstream OS and runtime release schedules: node:18 reached end-of-life in April 2025, after which Node.js upstream stopped publishing security patches for the 18.x branch and Docker's official Node.js image stopped pushing CVE-remediation updates for the node:18 tag. A fleet with pinned node:18 images after April 2025 stops receiving security patches through normal base image update operations; CVEs disclosed after the EOL date accumulate without remediation until the team migrates to an active LTS release. The EOL tracking cadence must specify how the team monitors upcoming EOL dates for the runtimes used in the fleet (Node.js publishes an LTS schedule at nodejs.org/en/about/previous-releases; Python publishes end-of-life dates at devguide.python.org/versions), the minimum advance notice before EOL at which the team initiates a migration evaluation (commonly six months), and the maximum acceptable time between EOL and fleet migration completion. An approved base image list that enumerates only current LTS releases — and is reviewed at the beginning of each quarter against each runtime's release schedule — converts EOL risk from a surprise (discovered when a scanner returns "no fix available" for a base image CVE because the base image is past EOL) into a planned migration (initiated six months before EOL with a defined completion target).

The layer cache model and the build time ceiling

Docker's layer cache is the primary mechanism for keeping container image build times within an acceptable range as the codebase and its dependency set grow. A build with an effective cache configuration re-executes only the layers whose inputs changed since the last build: a source code change re-runs the application compilation step but not the dependency installation step; a package.json change re-runs the dependency installation step from that point forward. A build with an ineffective cache configuration re-executes all layers on every build, regardless of what changed, because an early layer's inputs changed and invalidated the cache for all subsequent layers. The anti-pattern that eliminates cache effectiveness is present in most Dockerfiles written in founding containerization sessions: COPY . . before RUN npm install. When the full source directory is copied first, any change to any source file (including files that have no relationship to dependencies) invalidates the source COPY layer, which invalidates the npm install layer, which means npm install runs from scratch on every build even when no dependency changed. For a project with 400 npm dependencies and a 30-second install time, this anti-pattern adds 30 seconds to every CI build for every source file change — the cost of a missing four-line fix in the Dockerfile.

The correct layer ordering for dependency-install steps is a two-step COPY: first copy only the dependency manifest files (package.json, package-lock.json, requirements.txt, Cargo.toml, go.mod), then run the install step, then copy the full source. Since the dependency manifest files change only when a dependency is added, removed, or updated — not when application source changes — the install layer remains cached across all source-only changes. The dependency manifest COPY step is a four-line addition to the Dockerfile that applies to every language runtime: Node.js, Python, Go, Rust, Ruby, Java (pom.xml or build.gradle before src/). The decision record's Dockerfile structure section must specify this ordering as a requirement, documented clearly enough that an engineer writing a new service Dockerfile can implement it without searching for examples in existing Dockerfiles.

BuildKit cache mounts extend the layer cache model to the package manager's internal cache. When npm install runs inside a Docker layer, npm downloads packages from the registry and caches them in the npm cache directory (default: /root/.npm). Without a BuildKit cache mount, this cache is created inside the layer during the build and is included in the layer's stored content — adding to the image size — and is discarded with the layer when the layer's cache is invalidated. With a BuildKit cache mount (RUN --mount=type=cache,target=/root/.npm npm ci), the npm cache directory persists in BuildKit's own cache store between builds, separate from the image layer contents. When the dependency manifest changes and the npm install layer is invalidated, npm downloads only the packages that changed — not all packages — because the unchanged packages are already in the BuildKit cache. For a project with 400 dependencies where 395 remain unchanged between two builds, the difference between a full install from scratch and a BuildKit-cached install is the download time for 5 packages versus 400. The build time budget must be defined per image type: development images (used for local testing, may include additional debugging tools) have a more relaxed budget than production images (must be buildable in under two minutes with a warm cache); test images (used in CI to run the test suite) have a budget determined by the CI concurrency model. The budget is measured by CI pipeline instrumentation — a step in the CI configuration that records the build step duration — and reviewed when a PR's build time exceeds the budget, triggering a Dockerfile review rather than a capacity increase in the CI runner.

CI-level cache configuration is separate from Docker's layer cache and from BuildKit cache mounts. Without CI-level cache configuration, the Docker layer cache is populated on the CI runner's local storage and is discarded when the runner terminates. Each new CI job starts on a fresh runner (in most cloud-hosted CI environments: GitHub Actions, GitLab CI managed runners, CircleCI cloud) with no layer cache from previous builds. The effective result is that the layer ordering optimizations and BuildKit cache mounts produce no benefit — every build is a cache miss at the CI level. The CI-level cache for Docker builds is configured using the build cache import and export mechanism: --cache-from type=registry,ref=registry.example.com/myapp:buildcache and --cache-to type=registry,ref=registry.example.com/myapp:buildcache,mode=max. This stores the BuildKit cache in the team's container registry and restores it at the beginning of each CI build. The mode=max option stores cache entries for all intermediate layers, not just the final image — necessary for multi-stage builds where intermediate stages (the build stage) are not pushed to the registry but should be cached. GitHub Actions also provides an actions/cache-backed BuildKit cache through the gha cache backend, which uses GitHub's hosted cache storage and avoids the registry pull cost of the registry-backend option. The decision record must specify which CI-level cache backend the team uses, because the choice determines whether the layer ordering and BuildKit cache mount optimizations produce their intended build time reduction.

The image registry and the supply chain coupling surface

The image registry is the artifact store for the container fleet's build outputs and the upstream source for base images used across the fleet. The registry's supply chain policies — where base images are pulled from, how images are signed, what metadata is attached to each image at build time, and how long images are retained — determine the team's exposure to upstream registry failures, image tampering between build and deployment, and license or vulnerability compliance obligations. These policies have no visible cost in the first year of a containerized deployment; their cost becomes visible when an upstream registry goes down during a critical deployment, when an image in the registry is found to have been modified after the build that produced it, or when a customer requests an SBOM for compliance reasons and the team has no mechanism to produce one.

Base image pull-through caching eliminates the upstream registry dependency from the build pipeline's critical path. Without a pull-through cache, every Docker build that encounters a FROM node:20-alpine instruction that is not in the local layer cache must pull the base image from Docker Hub. Docker Hub's rate limits (100 pulls per 6 hours for unauthenticated requests, 200 per 6 hours for authenticated free accounts) can throttle CI pipelines in teams with many services and frequent builds. More critically, base image tags are not immutable: FROM node:20-alpine resolves to a specific image digest at build time, but the digest that node:20-alpine resolves to can change if the Node.js maintainers push an updated image under the same tag. A build that uses the tag reference rather than the digest reference may produce a different image from the same Dockerfile on two consecutive build runs if the tag was updated between runs. The correct policy is to pin all base image references to immutable digest references (FROM node:20-alpine@sha256:abc123...) and to update the digest reference through a controlled process — a bot that opens PRs when upstream base images are updated, allowing the team to review the change log and approve the update explicitly rather than inheriting it silently through tag mutation. A pull-through cache in the team's own registry (AWS ECR Public Gallery, Google Artifact Registry remote repository, Harbor) also provides availability during Docker Hub outages: the pull-through cache serves the last-pulled version of the base image from the team's registry if Docker Hub is unavailable, preventing Docker Hub's status from becoming the team's deployment pipeline's status.

Image signing with Sigstore/Cosign provides cryptographic assurance that an image in the registry was produced by the team's CI pipeline and has not been modified after the build that produced it. The signing step runs immediately after the docker push step in the CI pipeline: Cosign signs the image's OCI manifest digest using an OIDC identity token from the CI provider (GitHub Actions, GitLab CI), which eliminates the need for a long-lived signing key stored as a CI secret. The signature is stored in the same registry as the image, attached to the image manifest, and is verifiable with the public key derived from the OIDC issuer's public certificate. Kubernetes admission control policies (Kyverno, OPA/Gatekeeper, Sigstore's Policy Controller) can verify image signatures before admitting a container to the production cluster, ensuring that only images built by the team's CI pipeline can run in production. The signing policy must specify: the signing key model (keyless signing via OIDC is preferred over long-lived key management), the registries where signatures are stored, and whether signature verification is enforced at deployment time or audited post-deployment. The CI/CD pipeline decision record must be consistent with the image signing policy: the signing step is part of the CI pipeline's build-and-push stage, and a change to the CI pipeline's OIDC configuration or the signing step's invocation changes the provenance claims attached to all subsequently-built images.

Software Bill of Materials (SBOM) generation attaches a machine-readable inventory of all packages in the image at build time. Generated by Syft or Trivy at the end of the build pipeline and attached to the image manifest as an OCI artifact, the SBOM enables post-build vulnerability impact analysis: when a new CVE is disclosed, the SBOM for each image in the registry can be queried to determine which images contain the affected package version, without re-scanning the full image. It also enables license compliance auditing (identifying GPL or other restricted-license packages before they reach production) and satisfies enterprise customer requirements for a component inventory. The SBOM generation policy must specify the SBOM format (SPDX, CycloneDX — CycloneDX has broader tool support for container image SBOMs as of 2025), the attachment mechanism (OCI artifact referrer, attached to the image manifest's referrers list), and whether SBOMs are generated for all images or only for production-tagged images. Retention policy governs the growth of the registry's storage footprint: each pushed image adds a new manifest and a set of layer blobs to the registry. Without lifecycle rules, untagged manifests accumulate as old images are superseded by new builds — a registry with 24 deploys per day across twelve services accumulates 288 image manifests per day, not counting the layer blobs shared between images. The lifecycle rules must specify which images are exempt from automatic deletion (semantic-version-tagged releases, images referenced in currently-deployed production workloads) and which are eligible for expiry (commit-SHA-tagged images older than 30 days, PR-tagged images whose PR is closed). The infrastructure-as-code decision record must cover the registry resource itself, its access control configuration, lifecycle rules, and cross-region replication policy as infrastructure provisioned by code rather than configured manually in the cloud console.

What a container image build policy decision record must document

The first section documents the base image selection policy. The approved base image list must enumerate the approved base image per language runtime, specifying the OS family (Debian/Alpine/Distroless), the variant (full/slim/alpine/distroless), the current approved tag (including LTS version), and the selection criteria that distinguish when each variant is appropriate. The native addon compatibility requirement must specify that services using native addons must verify the addon's libc compatibility before selecting a base image variant, and must use glibc-based images for addons that have not been verified against musl. The multi-stage build requirement must specify that services requiring build-time compilation tools must separate the build stage (which may use a full or slim image with a compiler) from the runtime stage (which must use an approved production base image without build tools). The EOL tracking cadence must specify the review frequency, the advance notice period before EOL at which migration planning begins, and the maximum acceptable time between EOL and fleet migration completion. The CI Dockerfile lint step must be specified: a check that validates FROM instructions in every Dockerfile against the approved base image list, failing CI for non-approved base images and producing an actionable error message that links to the approved image list.

The second section documents the Dockerfile structure requirements. The layer ordering requirement must specify the two-step dependency manifest COPY pattern for each language runtime used in the fleet, with a concrete example Dockerfile for each runtime that demonstrates the correct ordering. The non-root user requirement must specify that production containers must not run as root: a USER instruction in the Dockerfile switching to a non-root UID must be present in every production image, and the application must be configured to bind to ports above 1024 (the privilege threshold in Linux kernel capabilities) or the container runtime must grant the specific capability required for privileged port binding without root. The multi-stage build pattern must be shown with an example that illustrates the separation between build and runtime stages, the COPY --from=build instruction that transfers compiled outputs, and the absence of build tools in the runtime stage's filesystem. The .dockerignore requirement must specify that a .dockerignore file must be present in every service repository and must exclude development artifacts (node_modules, __pycache__, .git, *.log, coverage/) that should not be copied into the build context — a missing .dockerignore in a Node.js project causes COPY . . to include the local node_modules directory in the build context, transferring hundreds of megabytes to the Docker build process on every build and invalidating the layer cache's effectiveness.

The third section documents the build time policy. The layer cache configuration must specify which CI cache backend the team uses (GitHub Actions GHA cache, registry-based cache, BuildKit depot) and the configuration for each CI environment where images are built. The BuildKit cache mount configuration must list the cache mount path per package manager and the cache mount configuration format for the CI environment. The build time budget must specify the maximum acceptable build time per image type (development, production, test) as a CI metric measured in the pipeline and reported when exceeded. The budget review trigger must specify when a build time regression (a PR that increases build time above the budget) requires a Dockerfile review and optimization before merge versus when it is acceptable to merge with a tracked remediation task.

The fourth section documents the vulnerability scanning policy. The scanner selection must specify which tool is used (Trivy, Grype, Snyk Container), its version pinning in the CI configuration, and its CVE database update frequency. The scan trigger must specify that scanning runs: at PR merge gate (blocking on CRITICAL and HIGH with a fix available), as a scheduled daily registry scan (alerting on newly-disclosed CVEs in already-deployed images), and as a manual scan on-demand when a critical CVE is publicly disclosed. The severity threshold must specify which severity levels block a merge, which are reported as warnings, and which are suppressed. The exception process must specify the format for CVE exceptions (a structured file in the repository with the CVE identifier, the reason the CVE is accepted without immediate remediation, the review date, and the approver), the maximum acceptable duration of a CVE exception before re-evaluation (commonly 30 days for HIGH, 7 days for CRITICAL with no available fix), and the escalation path for exceptions that exceed the review date. The rebuild cadence must specify the maximum time between a CVE disclosure in a base image package and the team's rebuild of all affected images using an updated base image that includes the patch.

The fifth section documents the registry and supply chain policy. The registry selection must specify the primary registry (ECR, GCR, Artifact Registry, Docker Hub private), the pull-through cache configuration for upstream base images, and the access control model (per-service repository vs. shared repository, push permissions restricted to CI service accounts, pull permissions for deployment service accounts). The image naming convention must specify the tag format per tag type (production: service-name:git-sha-semver, development: service-name:branch-name-sha, test: service-name:pr-NNN). The retention policy must specify the lifecycle rules and their parameters. The image signing policy must specify the signing key model, the signing step location in the CI pipeline, and the deployment admission control configuration. The SBOM policy must specify the SBOM format, generation trigger, attachment mechanism, and retention duration. The secrets handling requirement must specify that build-time secrets are passed using BuildKit secret mounts and never through ENV or ARG instructions, with a CI lint step that detects ENV instructions containing the words KEY, SECRET, TOKEN, PASSWORD, or CREDENTIAL as a heuristic for secrets baked into the image. The secrets management decision record must be consistent with this requirement: the secrets that appear in the runtime environment (through Kubernetes Secrets, AWS Secrets Manager injection, or environment variable injection by the container orchestrator) are a different set from the secrets that are required at build time, and the two categories must be handled through separate mechanisms with no overlap.

The first containerization session, the CI integration session, and the production deployment session each produce container image decisions whose long-term operational cost — a six-week CVE remediation audit across a twelve-service fleet because no vulnerability scan was integrated and no base image selection policy was written; or an eight-day coordinated migration to eliminate a musl/glibc rounding discrepancy that passed all tests for four months before a customer's transaction volume exposed it — exceeds what an approved base image list, a Dockerfile structure requirement, a vulnerability scan integration, and an image signing policy would have cost at the time the founding decisions were made. The decisions are in the AI chat history: the first containerization session that chose node:18 for convenience rather than documentation, the CI integration session that added docker build to the pipeline without a cache configuration or a scan step, the production deployment session that configured the registry without a retention lifecycle rule or an image signing step. WhyChose's open-source extractor surfaces these founding infrastructure sessions as structured decision records before the next CVE audit reveals that twelve services have been accumulating vulnerabilities without a scan integration for twenty-two months, or before a base image variant mismatch produces a behavior regression that passes every test in the suite and surfaces through a customer escalation that requires coordinated migration across the full container fleet. The new CTO onboarding problem in the context of a containerized architecture is an image policy audit problem: an incoming technical leader will find a fleet with no approved base image list, multi-stage build patterns applied inconsistently, build times of 12–18 minutes on services where a Dockerfile fix and a CI cache configuration would produce sub-2-minute builds, no vulnerability scan integration, and no image signing policy — or no document explaining why any of those choices were made and under what conditions they should change. The decisions that were never written down in the container image context are the base image selection criteria that would have prevented the variant mismatch, the layer ordering requirement that would have kept CI builds under two minutes, and the CVE scan threshold that would have surfaced 312 high-severity vulnerabilities in month three rather than month twenty-three.

Further reading