From b541a6ab9006a45e9ac985a98568b280d0d9127d Mon Sep 17 00:00:00 2001 From: Leo Di Donato <120051+leodido@users.noreply.github.com> Date: Wed, 19 Nov 2025 16:22:34 +0000 Subject: [PATCH 1/2] feat(docker): pass SOURCE_DATE_EPOCH as build arg for deterministic images Pass SOURCE_DATE_EPOCH as a Docker build arg to enable deterministic Docker image timestamps. Dockerfiles MUST declare ARG SOURCE_DATE_EPOCH for BuildKit to use the timestamp for image metadata: FROM alpine:3.18 ARG SOURCE_DATE_EPOCH Without this ARG declaration, images will have non-deterministic timestamps even though the environment variable is set (from PR #284). With the ARG, BuildKit uses SOURCE_DATE_EPOCH for: - Image metadata timestamps (created field) - History timestamps - OCI annotations The ARG is also available in RUN commands for custom build logic: RUN go build -ldflags "-X main.BuildTime=$SOURCE_DATE_EPOCH" -o app Co-authored-by: Ona --- README.md | 31 ++++++++++++++++++++++++++++++- pkg/leeway/build.go | 2 ++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 05503253..62059d12 100644 --- a/README.md +++ b/README.md @@ -329,10 +329,39 @@ packages: **Docker builds:** -For Docker packages, leeway automatically enables BuildKit (`DOCKER_BUILDKIT=1`) and exports `SOURCE_DATE_EPOCH`. For deterministic Docker images, see PR #285 which passes the value as a build arg (requires `ARG SOURCE_DATE_EPOCH` in Dockerfiles). +For Docker packages, leeway automatically enables BuildKit (`DOCKER_BUILDKIT=1`) and exports `SOURCE_DATE_EPOCH`. Additionally, leeway passes `SOURCE_DATE_EPOCH` as a build arg to enable deterministic image timestamps. BuildKit is the default builder since Docker Engine v23.0 and is always used in Docker Desktop. +**Dockerfile requirements for deterministic images:** + +Dockerfiles MUST declare the build arg for BuildKit to use the timestamp for image metadata: + +```dockerfile +FROM alpine:3.18 +ARG SOURCE_DATE_EPOCH +COPY app /usr/local/bin/app +``` + +With the `ARG SOURCE_DATE_EPOCH` declaration, BuildKit (>= v0.13) automatically uses the timestamp for: +- Layer creation timestamps +- Image config `created` timestamp +- History timestamps +- OCI annotations + +Without the ARG declaration, images will have non-deterministic timestamps even though leeway sets the environment variable. + +For multi-stage builds, declare the ARG in each stage: + +```dockerfile +FROM golang:1.21 AS builder +ARG SOURCE_DATE_EPOCH +RUN go build -o app + +FROM alpine:3.18 +ARG SOURCE_DATE_EPOCH +COPY --from=builder /app /app +``` ## Package Variants Leeway supports build-time variance through "package variants". Those variants are defined on the workspace level and can modify the list of sources, environment variables and config of packages. For example consider a `WORKSPACE.YAML` with this variants section: diff --git a/pkg/leeway/build.go b/pkg/leeway/build.go index 1af3e351..7c6e76b6 100644 --- a/pkg/leeway/build.go +++ b/pkg/leeway/build.go @@ -1992,6 +1992,8 @@ func (p *Package) buildDocker(buildctx *buildContext, wd, result string) (res *p buildcmd = append(buildcmd, "--build-arg", fmt.Sprintf("DEP_%s=%s", arg, val)) } buildcmd = append(buildcmd, "--build-arg", fmt.Sprintf("__GIT_COMMIT=%s", p.C.Git().Commit)) + // Pass SOURCE_DATE_EPOCH for deterministic Docker image timestamps + buildcmd = append(buildcmd, "--build-arg", fmt.Sprintf("SOURCE_DATE_EPOCH=%d", mtime)) if cfg.Squash { buildcmd = append(buildcmd, "--squash") } From 4b74f6348acdac5c259d0940bfa4b13c5390f86f Mon Sep 17 00:00:00 2001 From: Leo Di Donato <120051+leodido@users.noreply.github.com> Date: Wed, 19 Nov 2025 19:31:33 +0000 Subject: [PATCH 2/2] fix(docker): use deterministic timestamp in docker-export-metadata.json Use getDeterministicMtime() for BuildTime in docker-export-metadata.json instead of time.Now() to ensure deterministic metadata files. This makes the docker-export-metadata.json file reproducible across builds with the same source code, reducing non-determinism in exported Docker image cache archives. The timestamp is derived from: - Git commit timestamp (normal case) - SOURCE_DATE_EPOCH env var (override) - Returns 0 in test environments Co-authored-by: Ona --- pkg/leeway/build.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/leeway/build.go b/pkg/leeway/build.go index 7c6e76b6..f11d71b4 100644 --- a/pkg/leeway/build.go +++ b/pkg/leeway/build.go @@ -2221,7 +2221,11 @@ func (p *Package) buildDocker(buildctx *buildContext, wd, result string) (res *p // Add PostProcess to create structured metadata file res.PostProcess = func(buildCtx *buildContext, pkg *Package, buildDir string) error { - return createDockerExportMetadata(buildDir, version, cfg) + mtime, err := pkg.getDeterministicMtime() + if err != nil { + return fmt.Errorf("failed to get deterministic mtime: %w", err) + } + return createDockerExportMetadata(buildDir, version, cfg, mtime) } // Add subjects function for provenance generation @@ -2367,11 +2371,11 @@ type DockerImageMetadata struct { } // createDockerExportMetadata creates metadata file for exported Docker images -func createDockerExportMetadata(wd, version string, cfg DockerPkgConfig) error { +func createDockerExportMetadata(wd, version string, cfg DockerPkgConfig, mtime int64) error { metadata := DockerImageMetadata{ ImageNames: cfg.Image, BuiltVersion: version, - BuildTime: time.Now(), + BuildTime: time.Unix(mtime, 0), CustomMeta: cfg.Metadata, }