Skip to content

[BUG] x-bake extension fields are silently dropped when building via COMPOSE_BAKE=true #13724

@elijahchancey

Description

@elijahchancey

Description

When COMPOSE_BAKE=true docker compose build is used to build a service, Docker Compose's internal compose→bake translator silently ignores the x-bake extension on the service's build:
block. Specifically, x-bake.output, x-bake.secrets, x-bake.platforms, x-bake.cache-from, x-bake.pull, x-bake.no-cache, and the other documented x-bake
fields
never reach the bake target that Compose constructs.

This is surprising because:

  1. docker compose config shows the x-bake extension is loaded and present in the merged compose config.
  2. docker buildx bake -f compose.yml (invoking bake directly on the same compose file) does honor the extension — the parsed bake target includes exactly the fields from x-bake.
  3. The x-bake docs explicitly describe x-bake as "the special extension field… to evaluate extra fields" — implying it works with the bake
    integration that COMPOSE_BAKE=true enables.

The effect is that any compose file that uses x-bake to pass bake-only options (e.g., setting output: type=image,push=true,compression=zstd for zstd layer compression) works correctly with
docker buildx bake but is silently degraded to Compose's default output behavior when COMPOSE_BAKE=true is used.

Steps To Reproduce

Setup

mkdir /tmp/xbake-repro && cd /tmp/xbake-repro                        

Dockerfile:

FROM alpine:3.20                                                                                                                                                                                   
RUN dd if=/dev/urandom of=/blob bs=1M count=100                      

compose.yml:

services:
  demo:
    build:
      context: .
      dockerfile: Dockerfile
      x-bake:
        output:
          - type=image,name=xbake-repro:latest,compression=zstd,compression-level=3,force-compression=true,oci-mediatypes=true                                                                     

Expected behavior (works correctly)

docker buildx bake reading the compose file directly does honor x-bake.output:

$ docker buildx bake --print -f compose.yml demo                     
#1 [internal] load local bake definitions
#1 reading compose.yml 232B / 232B done                                                                                                                                                            
#1 DONE 0.0s                                                                                                                                                                                       
{                                                                                                                                                                                                  
  "group": { "default": { "targets": ["demo"] } },                                                                                                                                                 
  "target": {                                                                                                                                                                                      
    "demo": {
      "context": ".",                                                                                                                                                                              
      "dockerfile": "Dockerfile",                                    
      "output": [
        {                                                                                                                                                                                          
          "compression": "zstd",
          "compression-level": "3",                                                                                                                                                                
          "force-compression": "true",                               
          "name": "xbake-repro:latest",
          "oci-mediatypes": "true",                                                                                                                                                                
          "type": "image"
        }                                                                                                                                                                                          
      ]                                                              
    }
  }
}

Note the "compression": "zstd" etc. in the parsed output field — exactly as specified in the compose file's x-bake block.

Actual behavior (bug)

docker compose config confirms the extension is loaded:

$ docker compose -f compose.yml config
name: xbake-repro                                                                                                                                                                                  
services:
  demo:                                                                                                                                                                                            
    build:                                                           
      context: /tmp/xbake-repro
      dockerfile: Dockerfile
      x-bake:
        output:                                                                                                                                                                                    
          - type=image,name=xbake-repro:latest,compression=zstd,compression-level=3,force-compression=true,oci-mediatypes=true
    ...                                                                                                                                                                                            

But when building via COMPOSE_BAKE=true docker compose build, the resulting bake target has none of the x-bake.output fields. The output value is hardcoded by Compose's translator to one of
type=docker, type=registry, or type=image,push=%t — regardless of what x-bake.output says.

Root cause

In pkg/compose/build_bake.go, the doBuildBake function constructs the bake target's Outputs field from exactly three
sources:

var outputs []string
var call string                                                                                                                                                                                    
push := options.Push && service.Image != ""                          
switch {                                                                                                                                                                                           
case options.Check:
    call = "lint"                                                                                                                                                                                  
case len(service.Build.Platforms) > 1:                               
    outputs = []string{fmt.Sprintf("type=image,push=%t", push)}
default:                                                                                                                                                                                           
    if push {
        outputs = []string{"type=registry"}                                                                                                                                                        
    } else {                                                         
        outputs = []string{"type=docker"}
    }                                                                                                                                                                                              
}

grep -n "xbake\|x-bake\|Extensions" pkg/compose/build_bake.go on current main returns zero matches — the file doesn't read service.Build.Extensions["x-bake"] (or anywhere else) when
populating the bake target. The same holds for all the other x-bake fields (secrets, platforms, cache-from, etc.) documented at https://docs.docker.com/build/bake/compose-file/.

Prior art / related

  • #12956 [BUG] Not generating a "cache-to" field when COMPOSE_BAKE=true from yaml (closed, fixed by
    #12959) — same family of bug, fixed for cache_to by routing it through Compose's native cache_to field rather than via x-bake. The present
    issue is about x-bake extensions specifically, which is still ignored.
  • moby/buildkit#4898 — a different bug about YAML extension merging when a base compose file has an empty x-bake: key. Not the same code path.
  • docker/buildx#3072 — long-standing proposal for a top-level docker buildx build --compression flag. Making x-bake.output work via
    COMPOSE_BAKE would close a meaningful portion of that issue's use cases for compose users.

Proposed fix

Read service.Build.Extensions in doBuildBake and merge recognised x-bake fields into the bakeTarget struct. The set of fields to recognise is documented at
https://docs.docker.com/build/bake/compose-file/ — at minimum: output, secrets, platforms, tags, contexts, no-cache, no-cache-filter, pull, cache-from, cache-to, ssh.

The implementation shape is similar to #12959, which added a native cache_to code path — but generalised to deep-merge the x-bake extension onto
the bake target after the existing hardcoded logic runs, so user intent wins over defaults.

Happy to follow up with a PR if the approach is acceptable.

Real-world impact

The use case that led me to file this: I wanted to compress image layers with zstd (instead of gzip) on a large full Go development image (~3.5 GB uncompressed, including a 1.3 GB go build
cache baked into a layer). With gzip, the exporting layers step of the build takes ~70 seconds single-threaded. With zstd it drops to ~5 seconds (multi-threaded, ~4.5× better ratio on Go build
artefacts). The natural way to enable this is x-bake.output: type=image,compression=zstd,... in compose.yml and COMPOSE_BAKE=true on the build command. That's how the feature is documented
and that's how it works with docker buildx bake directly.

Because COMPOSE_BAKE=true docker compose build silently ignores the extension, we had to replace our docker-compose plugin invocation with a direct docker buildx build --output type=image,compression=zstd,... call, losing the plugin abstraction for that one step. Other users of the ecosystem (Buildkite users, GitHub Actions users, anyone wrapping compose) will hit the
same wall and won't have a good option.

Environment

  • Docker version: 29.3.1 (API 1.54), client 29.3.1
  • Docker Compose version: v5.1.1
  • docker buildx version: v0.32.1-desktop.1 (commit 56d7a98f3ce2e9c260d9f75460a8308e4f157a47)
  • Platform: macOS (Darwin 25.4.0)

Also reproduces against docker/compose main branch based on source-code inspection of pkg/compose/build_bake.go.

Compose Version

Docker Compose version v5.1.1

Docker Environment

docker info                                                                                                                                                                                            10:49:46
Client:
 Version:    29.3.1
 Context:    desktop-linux
 Debug Mode: false
 Plugins:
  agent: Docker AI Agent Runner (Docker Inc.)
    Version:  v1.39.0
    Path:     /Users/elijahchancey/.docker/cli-plugins/docker-agent
  ai: Docker AI Agent - Ask Gordon (Docker Inc.)
    Version:  v1.20.2
    Path:     /Users/elijahchancey/.docker/cli-plugins/docker-ai
  buildx: Docker Buildx (Docker Inc.)
    Version:  v0.32.1-desktop.1
    Path:     /Users/elijahchancey/.docker/cli-plugins/docker-buildx
  compose: Docker Compose (Docker Inc.)
    Version:  v5.1.1
    Path:     /Users/elijahchancey/.docker/cli-plugins/docker-compose
  debug: Get a shell into any image or container (Docker Inc.)
    Version:  0.0.47
    Path:     /Users/elijahchancey/.docker/cli-plugins/docker-debug
  desktop: Docker Desktop commands (Docker Inc.)
    Version:  v0.3.0
    Path:     /Users/elijahchancey/.docker/cli-plugins/docker-desktop
  dhi: CLI for managing Docker Hardened Images (Docker Inc.)
    Version:  v0.0.2
    Path:     /Users/elijahchancey/.docker/cli-plugins/docker-dhi
  extension: Manages Docker extensions (Docker Inc.)
    Version:  v0.2.31
    Path:     /Users/elijahchancey/.docker/cli-plugins/docker-extension
  init: Creates Docker-related starter files for your project (Docker Inc.)
    Version:  v1.4.0
    Path:     /Users/elijahchancey/.docker/cli-plugins/docker-init
  mcp: Docker MCP Plugin (Docker Inc.)
    Version:  v0.40.3
    Path:     /Users/elijahchancey/.docker/cli-plugins/docker-mcp
  model: Docker Model Runner (Docker Inc.)
    Version:  v1.1.28
    Path:     /Users/elijahchancey/.docker/cli-plugins/docker-model
  offload: Docker Offload (Docker Inc.)
    Version:  v0.5.81
    Path:     /Users/elijahchancey/.docker/cli-plugins/docker-offload
  pass: Docker Pass Secrets Manager Plugin (beta) (Docker Inc.)
    Version:  v0.0.24
    Path:     /Users/elijahchancey/.docker/cli-plugins/docker-pass
  sandbox: Docker Sandbox (Docker Inc.)
    Version:  v0.12.0
    Path:     /Users/elijahchancey/.docker/cli-plugins/docker-sandbox
  sbom: View the packaged-based Software Bill Of Materials (SBOM) for an image (Anchore Inc.)
    Version:  0.6.0
    Path:     /Users/elijahchancey/.docker/cli-plugins/docker-sbom
  scout: Docker Scout (Docker Inc.)
    Version:  v1.20.3
    Path:     /Users/elijahchancey/.docker/cli-plugins/docker-scout

Server:
 Containers: 6
  Running: 0
  Paused: 0
  Stopped: 6
 Images: 6
 Server Version: 29.3.1
 Storage Driver: overlayfs
  driver-type: io.containerd.snapshotter.v1
 Logging Driver: json-file
 Cgroup Driver: cgroupfs
 Cgroup Version: 2
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
 CDI spec directories:
  /etc/cdi
  /var/run/cdi
 Swarm: inactive
 Runtimes: io.containerd.runc.v2 runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: dea7da592f5d1d2b7755e3a161be07f43fad8f75
 runc version: v1.3.4-0-gd6d73eb8
 init version: de40ad0
 Security Options:
  seccomp
   Profile: builtin
  cgroupns
 Kernel Version: 6.12.76-linuxkit
 Operating System: Docker Desktop
 OSType: linux
 Architecture: aarch64
 CPUs: 14
 Total Memory: 7.652GiB
 Name: docker-desktop
 ID: 83d64803-9ddf-4d9d-ba78-518c4e490a1d
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 HTTP Proxy: http.docker.internal:3128
 HTTPS Proxy: http.docker.internal:3128
 No Proxy: hubproxy.docker.internal
 Labels:
  com.docker.desktop.address=unix:///Users/elijahchancey/Library/Containers/com.docker.docker/Data/docker-cli.sock
 Experimental: false
 Insecure Registries:
  hubproxy.docker.internal:5555
  ::1/128
  127.0.0.0/8
 Live Restore Enabled: false

Anything else?

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions