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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions