Dockerfile: COPY --link --from=<stage> / / copies the stage instead of merging its snapshot

Open
#7,375 3 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 2 days

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
43/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Active
Tech stack
docker, dockerfile, go

Research direction

Start with frontend/dockerfile/dockerfile2llb/convert_copy.go around lines 337–352, which the issue identifies as building the linked copy. Reproduce with BuildKit v0.34.2 and the provided Dockerfile, then inspect how source stages, Merge, and whiteouts are handled. Done means the eligible root-to-root linked copy reuses the stage snapshot without changing layer contents or behavior for cases with deletions.

Written by the indexing model from the issue text.

Description

COPY --link --from=<stage> / / writes a full new copy of the stage to disk.
The stage result is already a snapshot, so BuildKit could merge that snapshot on top of the base with no copy.

We build a matrix of toolchain images on one BuildKit daemon, for example 12 Go versions on 4 Ubuntu releases.
Each toolchain has a scratch stage that holds a trimmed tree, and several images copy that tree with COPY --link.
--link makes the copy independent of the base, so all images share it.
But it is still a second full copy of the scratch stage, and each stage between the download and the final image adds one more copy.
For 12 Go versions, that is about 3GB of extra disk on each builder for each extra stage.

Reproduce

BuildKit v0.34.2, docker-container driver, docker/dockerfile:1.

# syntax=docker/dockerfile:1
FROM scratch AS download
ADD --link --unpack=true --checksum=sha256:65efbc80fdc1fa2cd2631cad5b481777a68e8725f34e89f50fc6ac714e379b40 \
  https://go.dev/dl/go1.27.3.linux-amd64.tar.gz /

FROM scratch AS golang-binary
COPY --from=download --link --exclude=doc/** --exclude=test/** --exclude=api/** /go /usr/local/go

FROM ubuntu:jammy AS root
COPY --from=golang-binary --link / /
docker buildx build --target root --output type=cacheonly .
docker buildx du --verbose
Record Size
[download 1/1] ADD --link --unpack=true ... 295.8MB
[golang-binary 1/1] COPY --from=download --link --exclude=... 264.9MB
[root 2/3] COPY --from=golang-binary --link / / 264.9MB
[root 2/3] LINK COPY --from=golang-binary --link / / 5.9MB

The third record holds the same files as the second.

Cause

With --link, the Dockerfile frontend makes Merge(base, Scratch().File(Copy(src, ...))):
https://github.com/moby/buildkit/blob/v0.34.2/frontend/dockerfile/dockerfile2llb/convert_copy.go#L337-L352

The Copy runs even when the source is the root of another stage and the destination is /.

Proposal

If COPY --link --from=<stage> / / has no --chown, --chmod, --exclude or --parents, make Merge(base, <stage>) and skip the Copy.
The layer content is the same, and the stage is stored once.

A merge can give a different result from a copy when the stage has deleted files (whiteouts).
If that is a problem, the change could apply only to stages that are built from scratch with file operations.
A new Dockerfile flag for this case would also work for us.

Dominant language
Go
Stars
10.4k
Forks
1.6k
Avg merge
2d 20h
Merged PRs (30d)
72

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from moby/buildkit

All issues in moby/buildkit

Similar issues

More Go issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.