Skip to content

Conversation

@leodido
Copy link
Contributor

@leodido leodido commented Nov 19, 2025

Description

Pass SOURCE_DATE_EPOCH as a Docker build arg to enable deterministic Docker image timestamps.

Fixes https://linear.app/ona-team/issue/CLC-2097/improve-builds-determinism

Critical Requirement

Dockerfiles MUST declare ARG SOURCE_DATE_EPOCH for BuildKit to use the timestamp for image metadata. Without this declaration, images will have non-deterministic timestamps even though the environment variable is set.

How It Works

  1. PR feat: export SOURCE_DATE_EPOCH for build commands #284 exports SOURCE_DATE_EPOCH as an environment variable
  2. This PR passes it as --build-arg SOURCE_DATE_EPOCH=... to docker build
  3. Dockerfiles must declare ARG SOURCE_DATE_EPOCH to activate deterministic timestamps

Required Dockerfile Change

Add ARG SOURCE_DATE_EPOCH after the FROM statement:

FROM alpine:3.18
ARG SOURCE_DATE_EPOCH
# ... rest of Dockerfile

Important: The ARG must be declared after FROM because build args are scoped to the build stage. For multi-stage builds, declare it in each stage that needs it:

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

With this ARG declaration, BuildKit uses SOURCE_DATE_EPOCH for:

  • Image metadata timestamps (created field)
  • History timestamps
  • OCI annotations

Additional Use Cases

The ARG is also available in RUN commands for custom build logic:

ARG SOURCE_DATE_EPOCH
RUN echo "Build timestamp: $SOURCE_DATE_EPOCH" > /build-info.txt
RUN go build -ldflags "-X main.BuildTime=$SOURCE_DATE_EPOCH" -o app

Additional Fix: Deterministic Metadata

This PR also fixes a bug where docker-export-metadata.json was using time.Now() instead of the deterministic timestamp, making the cache tar.gz non-reproducible.

Before:

{
  "build_time": "2025-11-19T19:18:59.116392757Z"  // Different each build
}

After:

{
  "build_time": "2025-11-19T18:56:46Z"  // Deterministic from git commit
}

Note: The docker save output (image.tar) still has non-deterministic tar metadata. This will be addressed in a future PR. However, the Docker image itself (image ID and layers) remains fully deterministic.

Verification

Check BuildKit Version

Ensure you have BuildKit >= v0.13.0 for full SOURCE_DATE_EPOCH support:

docker buildx version
# Should show: github.com/docker/buildx v0.13.0 or later

For Docker Engine without buildx:

docker version --format '{{.Server.Version}}'
# Should be v23.0 or later (BuildKit is default)

Verify Deterministic Builds

Build the same package twice and compare checksums:

# First build
leeway build //:your-docker-package
IMAGE_ID_1=$(docker images -q your-image:latest)

# Clean and rebuild
docker rmi your-image:latest
leeway build //:your-docker-package
IMAGE_ID_2=$(docker images -q your-image:latest)

# Compare - should be identical
echo "Build 1: $IMAGE_ID_1"
echo "Build 2: $IMAGE_ID_2"
[ "$IMAGE_ID_1" = "$IMAGE_ID_2" ] && echo "✅ Deterministic!" || echo "❌ Non-deterministic"

Migration Guide

For teams with many Dockerfiles, use this script to add ARG SOURCE_DATE_EPOCH:

#!/bin/bash
# add-source-date-epoch.sh

find . -name "Dockerfile*" -type f | while read dockerfile; do
  # Check if ARG already exists
  if grep -q "^ARG SOURCE_DATE_EPOCH" "$dockerfile"; then
    echo "⏭️  Skipping $dockerfile (already has ARG)"
    continue
  fi
  
  # Add ARG after first FROM statement
  sed -i '/^FROM /a ARG SOURCE_DATE_EPOCH' "$dockerfile"
  echo "✅ Updated $dockerfile"
done

For multi-stage Dockerfiles, manually review and add ARG to each stage as needed.

Terminology Clarification

Deterministic images: The Docker image itself (layers, metadata, image ID) is identical across builds with the same source code. This is what BuildKit's SOURCE_DATE_EPOCH provides.

Reproducible cache: The leeway cache tar.gz file is also deterministic, meaning the same source produces the same cache artifact. This includes:

  • The Docker image (deterministic via BuildKit)
  • Metadata files like docker-export-metadata.json (fixed in this PR)
  • Note: docker save output may still have minor tar metadata differences, but the image content is identical

Testing

Verified with test Dockerfile:

  • Without ARG: Images have different timestamps on each build ❌
  • With ARG: Images have identical timestamps (deterministic) ✅

Changes

  • Add SOURCE_DATE_EPOCH as a build arg in buildDocker function
  • Use existing deterministic mtime value (derived from git commit timestamp)
  • Fix docker-export-metadata.json to use deterministic timestamp instead of time.Now()

References

@leodido leodido changed the title fix(docker): add SOURCE_DATE_EPOCH for deterministic images fix(docker): add SOURCE_DATE_EPOCH for deterministic images Nov 19, 2025
@leodido leodido self-assigned this Nov 19, 2025
@leodido leodido changed the base branch from main to leo/export-source-date-epoch November 19, 2025 18:32
@leodido leodido requested a review from kylos101 November 19, 2025 18:32
@leodido leodido changed the title fix(docker): add SOURCE_DATE_EPOCH for deterministic images feat(docker): pass SOURCE_DATE_EPOCH as build arg for Dockerfile access Nov 19, 2025
@leodido leodido force-pushed the leo/docker-source-date-epoch branch from 3f03680 to 133c4a2 Compare November 19, 2025 18:47
@leodido leodido changed the title feat(docker): pass SOURCE_DATE_EPOCH as build arg for Dockerfile access feat(docker): pass SOURCE_DATE_EPOCH as build arg for Dockerfile access Nov 19, 2025
@leodido leodido force-pushed the leo/export-source-date-epoch branch from 6754212 to 994b111 Compare November 19, 2025 18:52
@leodido leodido changed the title feat(docker): pass SOURCE_DATE_EPOCH as build arg for Dockerfile access feat(docker): pass SOURCE_DATE_EPOCH as build arg for deterministic images Nov 19, 2025
@leodido leodido force-pushed the leo/docker-source-date-epoch branch from 133c4a2 to a48f97d Compare November 19, 2025 19:04
@leodido leodido force-pushed the leo/export-source-date-epoch branch from 994b111 to be57173 Compare November 19, 2025 19:05
@leodido leodido force-pushed the leo/docker-source-date-epoch branch 2 times, most recently from 150ceb3 to 3772afa Compare November 19, 2025 19:17
@leodido leodido changed the title feat(docker): pass SOURCE_DATE_EPOCH as build arg for deterministic images feat(docker): pass SOURCE_DATE_EPOCH as build arg for deterministic images Nov 19, 2025
@leodido leodido force-pushed the leo/docker-source-date-epoch branch from 3772afa to 9f59f6d Compare November 19, 2025 19:19
Copy link

@kylos101 kylos101 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥳

@leodido leodido requested a review from geropl November 19, 2025 19:32
@leodido leodido force-pushed the leo/docker-source-date-epoch branch from 408e6b0 to 214d02b Compare November 19, 2025 19:36
@leodido leodido changed the title feat(docker): pass SOURCE_DATE_EPOCH as build arg for deterministic images feat(docker): pass SOURCE_DATE_EPOCH as build arg (+ fix timestamp in export metadata) for deterministic images Nov 19, 2025
@leodido leodido changed the title feat(docker): pass SOURCE_DATE_EPOCH as build arg (+ fix timestamp in export metadata) for deterministic images fix: pass SOURCE_DATE_EPOCH as build arg (+ fix timestamp in export metadata) for deterministic docker images Nov 19, 2025
Copy link
Member

@geropl geropl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes LGTM ✔️

@leodido leodido force-pushed the leo/export-source-date-epoch branch from be57173 to 015b47f Compare November 20, 2025 09:33
@leodido leodido changed the base branch from leo/export-source-date-epoch to main November 20, 2025 09:37
leodido and others added 2 commits November 20, 2025 09:38
…mages

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 <no-reply@ona.com>
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 <no-reply@ona.com>
@leodido leodido force-pushed the leo/docker-source-date-epoch branch from 214d02b to 4b74f63 Compare November 20, 2025 09:39
@leodido leodido merged commit 2667fac into main Nov 20, 2025
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants