Skip to content

Commit b8f0190

Browse files
WVerlaekona-agent
andcommitted
Add S3 Docker build cache support
Add support for S3-backed Docker build layer caching using BuildKit's type=s3 cache backend. This enables faster Docker builds by caching intermediate layers in S3. Configuration via environment variables: - LEEWAY_DOCKER_S3_CACHE_BUCKET: S3 bucket name (required) - LEEWAY_DOCKER_S3_CACHE_REGION: AWS region (required) - LEEWAY_DOCKER_S3_CACHE_PREFIX: Optional prefix for cache keys - LEEWAY_DOCKER_S3_CACHE_MODE: Cache mode (min/max, default: max) - LEEWAY_DOCKER_S3_CACHE_ENDPOINT: Custom S3 endpoint Also adds corresponding CLI flags for all options. When S3 cache is enabled, leeway automatically uses docker buildx build with --cache-from and --cache-to flags. Co-authored-by: Ona <no-reply@ona.com>
1 parent 7e12607 commit b8f0190

4 files changed

Lines changed: 409 additions & 1 deletion

File tree

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,39 @@ E.g. `component/nested:docker` becomes `COMPONENT_NESTED__DOCKER`.
209209

210210
See `leeway build --help` for more details.
211211

212+
#### S3 Docker Build Cache
213+
214+
Leeway supports S3-backed Docker build layer caching using BuildKit's `type=s3` cache backend. This can significantly speed up Docker builds by caching intermediate layers in S3.
215+
216+
**Configuration via environment variables:**
217+
```bash
218+
export LEEWAY_DOCKER_S3_CACHE_BUCKET=my-cache-bucket
219+
export LEEWAY_DOCKER_S3_CACHE_REGION=us-east-1
220+
export LEEWAY_DOCKER_S3_CACHE_PREFIX=docker-cache/ # optional
221+
export LEEWAY_DOCKER_S3_CACHE_MODE=max # optional: 'min' or 'max' (default: max)
222+
export LEEWAY_DOCKER_S3_CACHE_ENDPOINT=https://... # optional: for S3-compatible storage
223+
```
224+
225+
**Configuration via CLI flags:**
226+
```bash
227+
leeway build \
228+
--docker-s3-cache-bucket=my-cache-bucket \
229+
--docker-s3-cache-region=us-east-1 \
230+
--docker-s3-cache-prefix=docker-cache/ \
231+
:my-docker-package
232+
```
233+
234+
**Requirements:**
235+
- Docker Buildx (automatically used when S3 cache is configured)
236+
- AWS credentials configured (via environment variables, IAM role, or AWS config file)
237+
- S3 bucket with appropriate permissions
238+
239+
**Cache modes:**
240+
- `max`: Cache all layers including intermediate layers (better cache hit rate, more storage)
241+
- `min`: Cache only the final layer (less storage, fewer cache hits)
242+
243+
When S3 cache is enabled, leeway automatically switches to `docker buildx build` with `--cache-from` and `--cache-to` flags pointing to the configured S3 bucket.
244+
212245
### Generic packages
213246
```YAML
214247
config:

cmd/build.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,13 @@ func addBuildFlags(cmd *cobra.Command) {
241241
cmd.Flags().Bool("report-github", os.Getenv("GITHUB_OUTPUT") != "", "Report package build success/failure to GitHub Actions using the GITHUB_OUTPUT environment variable")
242242
cmd.Flags().Bool("fixed-build-dir", true, "Use a fixed build directory for each package, instead of based on the package version, to better utilize caches based on absolute paths (defaults to true)")
243243
cmd.Flags().Bool("docker-export-to-cache", false, "Export Docker images to cache instead of pushing directly (enables SLSA L3 compliance)")
244+
245+
// Docker S3 build cache flags
246+
cmd.Flags().String("docker-s3-cache-bucket", os.Getenv(leeway.EnvvarDockerS3CacheBucket), "S3 bucket for Docker build layer caching (defaults to $LEEWAY_DOCKER_S3_CACHE_BUCKET)")
247+
cmd.Flags().String("docker-s3-cache-region", os.Getenv(leeway.EnvvarDockerS3CacheRegion), "AWS region for Docker S3 cache bucket (defaults to $LEEWAY_DOCKER_S3_CACHE_REGION)")
248+
cmd.Flags().String("docker-s3-cache-prefix", os.Getenv(leeway.EnvvarDockerS3CachePrefix), "Prefix for Docker S3 cache keys (defaults to $LEEWAY_DOCKER_S3_CACHE_PREFIX)")
249+
cmd.Flags().String("docker-s3-cache-mode", os.Getenv(leeway.EnvvarDockerS3CacheMode), "Docker S3 cache mode: 'min' or 'max' (defaults to 'max')")
250+
cmd.Flags().String("docker-s3-cache-endpoint", os.Getenv(leeway.EnvvarDockerS3CacheEndpoint), "Custom S3 endpoint for Docker cache (for S3-compatible storage)")
244251
}
245252

246253
func getBuildOpts(cmd *cobra.Command) ([]leeway.BuildOption, cache.LocalCache) {
@@ -412,6 +419,29 @@ func getBuildOpts(cmd *cobra.Command) ([]leeway.BuildOption, cache.LocalCache) {
412419
dockerExportSet = true
413420
}
414421

422+
// Get Docker S3 cache configuration from CLI flags (which default to env vars)
423+
dockerS3CacheBucket, _ := cmd.Flags().GetString("docker-s3-cache-bucket")
424+
dockerS3CacheRegion, _ := cmd.Flags().GetString("docker-s3-cache-region")
425+
dockerS3CachePrefix, _ := cmd.Flags().GetString("docker-s3-cache-prefix")
426+
dockerS3CacheMode, _ := cmd.Flags().GetString("docker-s3-cache-mode")
427+
dockerS3CacheEndpoint, _ := cmd.Flags().GetString("docker-s3-cache-endpoint")
428+
429+
var dockerS3Cache *leeway.DockerS3CacheConfig
430+
if dockerS3CacheBucket != "" && dockerS3CacheRegion != "" {
431+
dockerS3Cache = &leeway.DockerS3CacheConfig{
432+
Bucket: dockerS3CacheBucket,
433+
Region: dockerS3CacheRegion,
434+
Prefix: dockerS3CachePrefix,
435+
Mode: dockerS3CacheMode,
436+
Endpoint: dockerS3CacheEndpoint,
437+
}
438+
log.WithFields(log.Fields{
439+
"bucket": dockerS3Cache.Bucket,
440+
"region": dockerS3Cache.Region,
441+
"prefix": dockerS3Cache.Prefix,
442+
}).Info("Docker S3 build cache enabled")
443+
}
444+
415445
return []leeway.BuildOption{
416446
leeway.WithLocalCache(localCache),
417447
leeway.WithRemoteCache(remoteCache),
@@ -430,6 +460,7 @@ func getBuildOpts(cmd *cobra.Command) ([]leeway.BuildOption, cache.LocalCache) {
430460
leeway.WithInFlightChecksums(inFlightChecksums),
431461
leeway.WithDockerExportToCache(dockerExportToCache, dockerExportSet),
432462
leeway.WithDockerExportEnv(dockerExportEnvValue, dockerExportEnvSet),
463+
leeway.WithDockerS3Cache(dockerS3Cache),
433464
}, localCache
434465
}
435466

pkg/leeway/build.go

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,21 @@ const (
102102
// EnvvarWorkspaceRoot names the environment variable for workspace root path
103103
EnvvarWorkspaceRoot = "LEEWAY_WORKSPACE_ROOT"
104104

105+
// EnvvarDockerS3CacheBucket names the S3 bucket for Docker build layer caching
106+
EnvvarDockerS3CacheBucket = "LEEWAY_DOCKER_S3_CACHE_BUCKET"
107+
108+
// EnvvarDockerS3CacheRegion names the AWS region for the S3 cache bucket
109+
EnvvarDockerS3CacheRegion = "LEEWAY_DOCKER_S3_CACHE_REGION"
110+
111+
// EnvvarDockerS3CachePrefix names the prefix for S3 cache keys (optional)
112+
EnvvarDockerS3CachePrefix = "LEEWAY_DOCKER_S3_CACHE_PREFIX"
113+
114+
// EnvvarDockerS3CacheMode controls the cache mode (min or max, defaults to max)
115+
EnvvarDockerS3CacheMode = "LEEWAY_DOCKER_S3_CACHE_MODE"
116+
117+
// EnvvarDockerS3CacheEndpoint allows specifying a custom S3 endpoint (for S3-compatible storage)
118+
EnvvarDockerS3CacheEndpoint = "LEEWAY_DOCKER_S3_CACHE_ENDPOINT"
119+
105120
// dockerImageNamesFiles is the name of the file store in poushed Docker build artifacts
106121
// which contains the names of the Docker images we just pushed
107122
dockerImageNamesFiles = "imgnames.txt"
@@ -492,12 +507,70 @@ type buildOptions struct {
492507
DockerExportEnvValue bool // Value from explicit user env var
493508
DockerExportEnvSet bool // Whether user explicitly set env var (before workspace)
494509

510+
// Docker S3 build cache configuration
511+
DockerS3Cache *DockerS3CacheConfig
512+
495513
context *buildContext
496514
}
497515

498516
// DockerBuildOptions are options passed to "docker build"
499517
type DockerBuildOptions map[string]string
500518

519+
// DockerS3CacheConfig configures S3-backed Docker build layer caching.
520+
// When configured, leeway adds --cache-from and --cache-to flags to docker buildx commands.
521+
type DockerS3CacheConfig struct {
522+
// Bucket is the S3 bucket name (required)
523+
Bucket string
524+
// Region is the AWS region for the bucket (required)
525+
Region string
526+
// Prefix is an optional prefix for cache keys (e.g., "cache/myproject/")
527+
Prefix string
528+
// Mode controls cache export mode: "min" (default layers only) or "max" (all layers)
529+
// Defaults to "max" for better cache hit rates
530+
Mode string
531+
// Endpoint is an optional custom S3 endpoint for S3-compatible storage
532+
Endpoint string
533+
}
534+
535+
// IsEnabled returns true if the S3 cache is properly configured
536+
func (c *DockerS3CacheConfig) IsEnabled() bool {
537+
return c != nil && c.Bucket != "" && c.Region != ""
538+
}
539+
540+
// CacheFromArg returns the --cache-from argument value for docker buildx
541+
func (c *DockerS3CacheConfig) CacheFromArg() string {
542+
if !c.IsEnabled() {
543+
return ""
544+
}
545+
arg := fmt.Sprintf("type=s3,region=%s,bucket=%s", c.Region, c.Bucket)
546+
if c.Prefix != "" {
547+
arg += fmt.Sprintf(",blobs_prefix=%s,manifests_prefix=%s", c.Prefix, c.Prefix)
548+
}
549+
if c.Endpoint != "" {
550+
arg += fmt.Sprintf(",endpoint_url=%s", c.Endpoint)
551+
}
552+
return arg
553+
}
554+
555+
// CacheToArg returns the --cache-to argument value for docker buildx
556+
func (c *DockerS3CacheConfig) CacheToArg() string {
557+
if !c.IsEnabled() {
558+
return ""
559+
}
560+
mode := c.Mode
561+
if mode == "" {
562+
mode = "max"
563+
}
564+
arg := fmt.Sprintf("type=s3,region=%s,bucket=%s,mode=%s", c.Region, c.Bucket, mode)
565+
if c.Prefix != "" {
566+
arg += fmt.Sprintf(",blobs_prefix=%s,manifests_prefix=%s", c.Prefix, c.Prefix)
567+
}
568+
if c.Endpoint != "" {
569+
arg += fmt.Sprintf(",endpoint_url=%s", c.Endpoint)
570+
}
571+
return arg
572+
}
573+
501574
// BuildOption configures the build behaviour
502575
type BuildOption func(*buildOptions) error
503576

@@ -640,6 +713,33 @@ func WithDockerExportEnv(value, isSet bool) BuildOption {
640713
}
641714
}
642715

716+
// WithDockerS3Cache configures S3-backed Docker build layer caching
717+
func WithDockerS3Cache(cfg *DockerS3CacheConfig) BuildOption {
718+
return func(opts *buildOptions) error {
719+
opts.DockerS3Cache = cfg
720+
return nil
721+
}
722+
}
723+
724+
// DockerS3CacheFromEnv creates a DockerS3CacheConfig from environment variables.
725+
// Returns nil if the required environment variables are not set.
726+
func DockerS3CacheFromEnv() *DockerS3CacheConfig {
727+
bucket := os.Getenv(EnvvarDockerS3CacheBucket)
728+
region := os.Getenv(EnvvarDockerS3CacheRegion)
729+
730+
if bucket == "" || region == "" {
731+
return nil
732+
}
733+
734+
return &DockerS3CacheConfig{
735+
Bucket: bucket,
736+
Region: region,
737+
Prefix: os.Getenv(EnvvarDockerS3CachePrefix),
738+
Mode: os.Getenv(EnvvarDockerS3CacheMode),
739+
Endpoint: os.Getenv(EnvvarDockerS3CacheEndpoint),
740+
}
741+
}
742+
643743
func withBuildContext(ctx *buildContext) BuildOption {
644744
return func(opts *buildOptions) error {
645745
opts.context = ctx
@@ -2380,19 +2480,43 @@ func (p *Package) buildDocker(buildctx *buildContext, wd, result string) (res *p
23802480
return nil, err
23812481
}
23822482

2383-
// Use buildx for OCI layout export when exporting to cache
2483+
// Determine if we need buildx (required for OCI export or S3 cache)
2484+
useS3Cache := buildctx.DockerS3Cache.IsEnabled()
2485+
useBuildx := *cfg.ExportToCache || useS3Cache
2486+
2487+
// Use buildx for OCI layout export when exporting to cache, or when S3 cache is enabled
23842488
var buildcmd []string
23852489
if *cfg.ExportToCache {
23862490
// Build with OCI layout export for deterministic caching
23872491
imageTarPath := filepath.Join(wd, "image.tar")
23882492
buildcmd = []string{"docker", "buildx", "build", "--pull"}
23892493
buildcmd = append(buildcmd, "--output", fmt.Sprintf("type=oci,dest=%s", imageTarPath))
23902494
buildcmd = append(buildcmd, "--tag", version)
2495+
} else if useBuildx {
2496+
// Use buildx for S3 cache support, but load to daemon for pushing
2497+
buildcmd = []string{"docker", "buildx", "build", "--pull", "--load", "-t", version}
23912498
} else {
23922499
// Normal build (load to daemon for pushing)
23932500
buildcmd = []string{"docker", "build", "--pull", "-t", version}
23942501
}
23952502

2503+
// Add S3 cache options if configured (only works with buildx)
2504+
if useS3Cache {
2505+
cacheFrom := buildctx.DockerS3Cache.CacheFromArg()
2506+
cacheTo := buildctx.DockerS3Cache.CacheToArg()
2507+
if cacheFrom != "" {
2508+
buildcmd = append(buildcmd, "--cache-from", cacheFrom)
2509+
}
2510+
if cacheTo != "" {
2511+
buildcmd = append(buildcmd, "--cache-to", cacheTo)
2512+
}
2513+
log.WithFields(log.Fields{
2514+
"bucket": buildctx.DockerS3Cache.Bucket,
2515+
"region": buildctx.DockerS3Cache.Region,
2516+
"prefix": buildctx.DockerS3Cache.Prefix,
2517+
}).Debug("Docker S3 build cache enabled")
2518+
}
2519+
23962520
for arg, val := range cfg.BuildArgs {
23972521
buildcmd = append(buildcmd, "--build-arg", fmt.Sprintf("%s=%s", arg, val))
23982522
}

0 commit comments

Comments
 (0)