skip to content

As a platform owner, how do you decide between buildpacks (bootBuildImage) and a hand-written Dockerfile, and what do you do to make buildpacks builds reproducible and secure in CI?

level: principalimportance: nice to knowfreq 25%

answer

  1. buildpacks: no Dockerfile, secure base, rebase to patch
  2. Dockerfile: full control, you own security/layering
  3. pin builder/runImage by @sha256 digest
  4. creds from CI secrets; scan + SBOM + sign
  5. rebase = patch OS without app rebuild

basics

~20 s

Buildpacks give maintenance-free, secure, well-layered images and automatic base-image patching, at the cost of control. Dockerfiles give full control but you own security/layering. For reproducibility, pin the builder/run images by digest, control the JVM version, manage registry creds as secrets, and scan the output.

solid answer

~40 s

Choose **buildpacks** when you want standardized, low-maintenance images across many services: no Dockerfile to review, community-maintained secure bases, automatic memory tuning and layering, and the ability to **rebase** to a patched run image without rebuilding the app. Choose a **Dockerfile** when you need bespoke base images, unusual OS packages, air-gapped builds, or precise control the buildpack doesn't expose. To harden buildpacks in CI: pin the `builder` and `runImage` by **digest** (not floating tags) for reproducibility; set `BP_JVM_VERSION` explicitly; inject `publishRegistry`/`builderRegistry` credentials from CI secrets, never in source; run the build against a controlled daemon; scan the produced image (Trivy/Grype) and enforce provenance/SBOM (Paketo emits SBOMs). Periodically re-pin to newer digests to pick up CVE fixes, and consider rebasing for fast base-image patching.

code

kotlin · 20 lines
kotlin
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage

tasks.named<BootBuildImage>("bootBuildImage") {
    imageName.set("ghcr.io/acme/orders:${project.version}")

    // Reproducibility: pin builder + run image by DIGEST, not a floating tag.
    builder.set("paketobuildpacks/builder-jammy-base@sha256:<pinned-digest>")
    runImage.set("paketobuildpacks/run-jammy-base@sha256:<pinned-digest>")

    environment.set(mapOf("BP_JVM_VERSION" to "21"))  // explicit runtime Java

    publish.set(true)
    docker {
        publishRegistry {
            username.set(providers.environmentVariable("REGISTRY_USER"))
            password.set(providers.environmentVariable("REGISTRY_TOKEN"))
            url.set("https://ghcr.io")
        }
    }
}

go deeper

for a junior

Knows buildpacks avoid writing a Dockerfile.

for a middle

Can articulate a few pros/cons and wire credentials from env.

for a senior

Weighs trade-offs, pins the JVM version, scans output, understands rebase.

for a principal

Owns fleet policy: digest pinning, secret scoping, SBOM/signing, patch cadence, and the buildpacks-vs-Dockerfile exception process.

**The decision framework.** *Buildpacks (`bootBuildImage`/`build-image`) strengths:* - **No Dockerfile to author, review, or drift** — one less thing per service; consistency across a fleet. - **Secure, minimal, maintained bases** (Paketo `-tiny` is distroless-like) with a small attack surface. - **Automatic optimizations:** layered-jar → OCI layers (caching), JVM **memory calculator**, sensible entrypoint. - **Rebase:** because CNB tracks metadata, you can swap in a **patched run image** (`pack rebase` / platform rebase) to fix an OS CVE **without rebuilding the application** — huge for fleet-wide patching. - **SBOM & provenance:** Paketo emits Software Bill of Materials for the image, aiding supply-chain compliance. *Buildpacks weaknesses:* - Less direct control over image internals; unusual OS packages or base images can be awkward. - Requires pulling large builder/run images — friction in **air-gapped** environments (though you can mirror them). - The build needs a container daemon; some CI setups make that harder (rootless/daemonless). *Dockerfile strengths:* total control, works with any base, easy to reason about line-by-line, no external builder dependency. *Weaknesses:* you now **own** layering, security patching, memory tuning, and multi-stage complexity for every service. (For completeness, **Jib** is a third option: daemonless, Dockerfile-less, but a different ecosystem than Boot's built-in buildpacks.) **Making buildpacks builds reproducible & secure in CI:** 1. **Pin by digest.** Floating tags like `paketobuildpacks/builder-jammy-base` move over time, so two builds of the same commit can differ (JRE patch level, OS packages). Pin `builder`/`runImage` to `...@sha256:<digest>` and bump deliberately. This gives reproducible, auditable builds. 2. **Pin the JVM version** with `BP_JVM_VERSION` so the runtime Java is explicit, not a moving default. 3. **Secrets, not source.** Supply `docker.builderRegistry`/`docker.publishRegistry` credentials from CI secret stores/env vars; never commit them. Use short-lived tokens where the registry supports it (ECR/GHCR OIDC). 4. **Control the daemon.** Point at a trusted daemon (`DOCKER_HOST`) or a rootless/isolated builder; avoid sharing a privileged daemon across untrusted jobs. 5. **Scan & sign.** Run image scanners (Trivy/Grype) on the output; generate/consume the Paketo **SBOM**; sign images (cosign) and enforce in admission control. 6. **Patch cadence.** Regularly re-pin to newer builder/run digests to absorb CVE fixes, and/or use **rebase** to patch the run image fleet-wide without app rebuilds. 7. **Determinism knobs.** CNB supports reproducible image timestamps (`SOURCE_DATE_EPOCH`)/creation-time settings so image digests are stable across identical builds — useful for verifiable pipelines. **Gotchas at scale:** - Not pinning digests undermines every reproducibility claim. - The convenience of auto-updating bases can also mean **silent drift** — pin + deliberate bumps beat surprises. - `-tiny` has no shell, which breaks `kubectl exec /bin/sh` debugging; standardize on a debug approach (ephemeral debug containers or a `-base` variant for certain services). - Publishing to the wrong registry because the image name host wasn't fully-qualified is a classic pipeline bug. **Bottom line:** default to buildpacks for uniform, low-toil, patchable, SBOM-bearing images; keep Dockerfiles for the minority of services with special needs; and in both cases pin, scope secrets, scan, and sign.

  • How can you patch an OS-level CVE in the base of many buildpacks images without rebuilding each application?
    Use CNB rebase: swap the app image's run-image layers for a patched run image via `pack rebase` (or platform tooling). Because CNB records the run-image boundary in metadata, the app layers are preserved and only the base is replaced — fast, fleet-wide patching.
  • Why pin the builder/run image by digest instead of a tag?
    Tags float — the same tag can point at new content over time, so identical source can yield different images (different JRE patch, OS packages). Pinning `@sha256:` makes builds reproducible and auditable; you then bump digests deliberately to absorb CVE fixes.

saying these in an interview costs you the question

  • Claiming buildpacks builds are automatically reproducible without pinning digests
  • Storing registry credentials in build files or the repo
  • Assuming -tiny images allow shell/exec debugging
  • Believing you must rebuild the whole app to patch an OS CVE (rebase exists)
  • Treating buildpacks and Dockerfiles as mutually exclusive org-wide with no room for exceptions

context