Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions define/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ type BuildOptions struct {
DefaultMountsFilePath string
// IIDFile tells the builder to write the image ID to the specified file
IIDFile string
// BuildIDFile tells the builder to write the build ID to the specified file
BuildIDFile string
// Squash tells the builder to produce an image with a single layer instead of with
// possibly more than one layer, by only committing a new layer after processing the
// final instruction.
Expand All @@ -276,6 +278,12 @@ type BuildOptions struct {
OnBuild []string
// Layers tells the builder to commit an image for each step in the Dockerfile.
Layers bool
// CacheStages tells the builder to preserve intermediate stage images instead of removing them.
CacheStages bool
// StageLabels tells the builder to add metadata labels to intermediate stage images for easier recognition.
// These labels include stage name, base image, build ID, and parent stage name (when a stage uses another
// intermediate stage as its base, i.e., transitive aliases). This option requires CacheStages to be enabled.
StageLabels bool
// NoCache tells the builder to build the image from scratch without checking for a cache.
// It creates a new set of cached images for the build.
NoCache bool
Expand Down
54 changes: 54 additions & 0 deletions docs/buildah-build.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ The value of `[name]` is matched with the following priority order:
* Stage defined with AS [name] inside Containerfile
* Image [name], either local or in a remote registry

**--build-id-file** *BuildIDfile*

Write a unique build ID (UUID) to the file. This build ID is generated once per
build and is added to intermediate stage images as a label (`io.buildah.build.id`)
when `--stage-labels` is enabled. This allows grouping all intermediate images
from a single build together. This option requires `--stage-labels` to be enabled.

**--cache-from**

Repository to utilize as a potential list of cache sources. When specified, Buildah will try to look for
Expand All @@ -136,6 +143,19 @@ the intermediate image is stored in the image itself. Buildah's approach is simi
does not inflate the size of the original image with intermediate images. Also, intermediate images can truly be
kept distributed across one or more remote registries using Buildah's caching mechanism.

**--cache-stages** *bool-value*

Preserve intermediate stage images instead of removing them after the build completes
(Default is `false`). By default, buildah removes intermediate stage images to save space.
This option keeps those images, which can be useful for debugging multi-stage builds or
for reusing intermediate stages in subsequent builds.

Note: This option only preserves stage images (FROM ... AS stage_name). It does not affect
the behavior of `--layers`, which controls per-instruction caching within stages.

When combined with `--stage-labels`, intermediate images will include metadata labels
for easier identification and management.

**--cache-to**

Set this flag to specify list of remote repositories that will be used to store cache images. Buildah will attempt to
Expand Down Expand Up @@ -1053,6 +1073,21 @@ To later use the ssh agent, use the --mount flag in a `RUN` instruction within a

`RUN --mount=type=secret,id=id mycmd`


**--stage-labels** *bool-value*

Add metadata labels to intermediate stage images (Default is `false`). This option
requires `--cache-stages` to be enabled.

When enabled, intermediate stage images will be labeled with:
- `io.buildah.stage.name`: The stage name (from `FROM ... AS name`)
- `io.buildah.stage.base`: The base image used by this stage
- `io.buildah.stage.parent_name`: The parent stage name (if this stage uses another stage as base)
- `io.buildah.build.id`: A unique build ID shared across all stages in a single build

These labels make it easier to identify, query, and manage intermediate images from
multi-stage builds.

**--stdin**

Pass stdin into the RUN containers. Sometimes commands being RUN within a Containerfile
Expand Down Expand Up @@ -1385,6 +1420,10 @@ buildah build -v /var/lib/dnf:/var/lib/dnf:O -t imageName .

buildah build --layers -t imageName .

buildah build --cache-stages --stage-labels -t imageName .

buildah build --cache-stages --stage-labels --build-id-file /tmp/build-id.txt -t imageName .

buildah build --no-cache -t imageName .

buildah build -f Containerfile --layers --force-rm -t imageName .
Expand Down Expand Up @@ -1443,6 +1482,21 @@ buildah build --output type=tar,dest=out.tar .

buildah build -o - . > out.tar

### Preserving and querying intermediate stage images

Build a multi-stage image while preserving intermediate stages with metadata labels:

buildah build --cache-stages --stage-labels --build-id-file /tmp/build-id.txt -t myapp .

Query intermediate images from a specific build using the build ID:

BUILD_ID=$(cat /tmp/build-id.txt)
buildah images --filter "label=io.buildah.build.id=${BUILD_ID}"

Find an intermediate image for a specific stage name:

buildah images --filter "label=io.buildah.stage.name=builder"

### Building an image using a URL

This will clone the specified GitHub repository from the URL and use it as context. The Containerfile or Dockerfile at the root of the repository is used as the context of the build. This only works if the GitHub repository is a dedicated repository.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/docker/go-connections v0.6.0
github.com/docker/go-units v0.5.0
github.com/fsouza/go-dockerclient v1.12.3
github.com/google/uuid v1.6.0
github.com/hashicorp/go-multierror v1.1.1
github.com/mattn/go-shellwords v1.0.12
github.com/moby/buildkit v0.26.2
Expand Down Expand Up @@ -79,7 +80,6 @@ require (
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-containerregistry v0.20.6 // indirect
github.com/google/go-intervals v0.0.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
Expand Down
40 changes: 37 additions & 3 deletions imagebuildah/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/containers/buildah/pkg/sshagent"
"github.com/containers/buildah/util"
encconfig "github.com/containers/ocicrypt/config"
"github.com/google/uuid"
digest "github.com/opencontainers/go-digest"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/openshift/imagebuilder"
Expand Down Expand Up @@ -111,6 +112,11 @@ type executor struct {
layerLabels []string
annotations []string
layers bool
cacheStages bool
stageLabels bool
buildID string
buildIDFile string
intermediateStageParents map[string]struct{} // Tracks which intermediate stages are used as base by other intermediate stages
noHostname bool
noHosts bool
useCache bool
Expand Down Expand Up @@ -248,6 +254,19 @@ func newExecutor(logger *logrus.Logger, logPrefix string, store storage.Store, o
buildOutputs = append(buildOutputs, options.BuildOutput) //nolint:staticcheck
}

// Generate unique build ID for stage labels (also used by --build-id-file)
var buildID string
if options.StageLabels {
buildID = uuid.New().String()

// Write build ID to file if requested
if options.BuildIDFile != "" {
if err := os.WriteFile(options.BuildIDFile, []byte(buildID), 0o644); err != nil {
return nil, fmt.Errorf("writing build ID to file %q: %w", options.BuildIDFile, err)
}
}
}

exec := executor{
args: options.Args,
cacheFrom: options.CacheFrom,
Expand Down Expand Up @@ -293,13 +312,18 @@ func newExecutor(logger *logrus.Logger, logPrefix string, store storage.Store, o
commonBuildOptions: options.CommonBuildOpts,
defaultMountsFilePath: options.DefaultMountsFilePath,
iidfile: options.IIDFile,
buildIDFile: options.BuildIDFile,
squash: options.Squash,
labels: slices.Clone(options.Labels),
layerLabels: slices.Clone(options.LayerLabels),
processLabel: processLabel,
mountLabel: mountLabel,
annotations: slices.Clone(options.Annotations),
layers: options.Layers,
cacheStages: options.CacheStages,
stageLabels: options.StageLabels,
buildID: buildID,
intermediateStageParents: make(map[string]struct{}),
noHostname: options.CommonBuildOpts.NoHostname,
noHosts: options.CommonBuildOpts.NoHosts,
useCache: !options.NoCache,
Expand Down Expand Up @@ -840,6 +864,15 @@ func (b *executor) Build(ctx context.Context, stages imagebuilder.Stages) (image
currentStageInfo.Needs = append(currentStageInfo.Needs, baseWithArg)
}
}
// Track if this base is another intermediate stage (used as parent by current intermediate stage)
if stageIndex < len(stages)-1 {
// Only mark if the base is actually a stage, not an external image
if _, ok := dependencyMap[baseWithArg]; ok {
b.intermediateStageParents[baseWithArg] = struct{}{}
logrus.Debugf("stage %d (%s) uses stage %q as base - marking %q as intermediate parent",
stageIndex, stage.Name, baseWithArg, baseWithArg)
}
}
}
}
case "ADD", "COPY":
Expand Down Expand Up @@ -1038,9 +1071,10 @@ func (b *executor) Build(ctx context.Context, stages imagebuilder.Stages) (image
// We're not populating the cache with intermediate
// images, so add this one to the list of images that
// we'll remove later.
// Only remove intermediate image is `--layers` is not provided
// or following stage was not only a base image ( i.e a different image ).
if !b.layers && !r.OnlyBaseImage {
// Only remove intermediate image if `--layers` is not provided,
// `--cache-stages` is not enabled, or following stage was not
// only a base image (i.e. a different image).
if !b.layers && !b.cacheStages && !r.OnlyBaseImage {
cleanupImages = append(cleanupImages, r.ImageID)
}
}
Expand Down
44 changes: 44 additions & 0 deletions imagebuildah/stage_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ type stageExecutor struct {
argsFromContainerfile []string
hasLink bool
isLastStep bool
fromName string // original FROM value (stage name or image name) before resolution to image ID
}

// Preserve informs the stage executor that from this point on, it needs to
Expand Down Expand Up @@ -1221,6 +1222,11 @@ func (s *stageExecutor) execute(ctx context.Context, base string) (imgID string,
return "", nil, false, err
}
pullPolicy := s.executor.pullPolicy
// Capture the original FROM value (stage/image name) before it's converted to image ID.
// Needed for indication of the transitive aliases when setting stage labels.
if s.fromName == "" {
s.fromName = base
}
s.executor.stagesLock.Lock()
var preserveBaseImageAnnotationsAtStageStart bool
if stageImage, isPreviousStage := s.executor.imageMap[base]; isPreviousStage {
Expand Down Expand Up @@ -1837,6 +1843,27 @@ func (s *stageExecutor) execute(ctx context.Context, base string) (imgID string,
s.hasLink = false
}

// If --cache-stages is enabled and this is not the last stage, commit the intermediate stage image.
// However, skip committing if this stage is a "parent" stage used as a base
// by another intermediate stage (transitive alias pattern).
// Only commit the final stage in a transitive alias chain.
if s.executor.cacheStages && !lastStage {
// Check if this stage is used as base by another intermediate stage
_, isParentStage := s.executor.intermediateStageParents[s.name]
if isParentStage {
logrus.Debugf("Skipping commit for intermediate stage %s (index %d) - used as base by another intermediate stage", s.name, s.index)
} else {
logrus.Debugf("Committing intermediate stage %s (index %d) for --cache-stages", s.name, s.index)
createdBy := fmt.Sprintf("/bin/sh -c #(nop) STAGE %s", s.name)
// Commit the stage without squashing, using empty output name (intermediate image)
imgID, commitResults, err = s.commit(ctx, createdBy, false, "", false, false)
if err != nil {
return "", nil, false, fmt.Errorf("committing intermediate stage %s: %w", s.name, err)
}
logrus.Debugf("Committed intermediate stage %s with ID %s", s.name, imgID)
}
}

return imgID, commitResults, onlyBaseImage, nil
}

Expand Down Expand Up @@ -2548,6 +2575,23 @@ func (s *stageExecutor) commit(ctx context.Context, createdBy string, emptyLayer
for k, v := range config.Labels {
s.builder.SetLabel(k, v)
}
// Add stage metadata labels if --cache-stages and --stage-labels are enabled.
// IMPORTANT: This must be done AFTER copying config.Labels to ensure stage labels
// are not overwritten by inherited labels from parent stages (transitive aliases).
if output == "" && s.executor.cacheStages && s.executor.stageLabels {
s.builder.SetLabel("io.buildah.stage.name", s.name)
s.builder.SetLabel("io.buildah.stage.base", s.builder.FromImage)

// Check if the base is another stage (transitive alias) using the original FROM value.
// s.fromName contains the stage name before resolution to image ID.
if s.fromName != "" && s.executor.stages[s.fromName] != nil {
s.builder.SetLabel("io.buildah.stage.parent_name", s.fromName)
}

if s.executor.buildID != "" {
s.builder.SetLabel("io.buildah.build.id", s.executor.buildID)
}
}
switch s.executor.commonBuildOptions.IdentityLabel {
case types.OptionalBoolTrue:
s.builder.SetLabel(buildah.BuilderIdentityAnnotation, define.Version)
Expand Down
11 changes: 11 additions & 0 deletions pkg/cli/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,14 @@ func GenBuildOptions(c *cobra.Command, inputArgs []string, iopts BuildOptions) (
return options, nil, nil, errors.New("'rm' and 'force-rm' can only be set with either 'layers' or 'no-cache'")
}

if iopts.StageLabels && !iopts.CacheStages {
return options, nil, nil, errors.New("'stage-labels' requires 'cache-stages'")
}

if iopts.BuildIDFile != "" && !iopts.StageLabels {
return options, nil, nil, errors.New("'build-id-file' requires 'stage-labels'")
}

if c.Flag("compress").Changed {
logrus.Debugf("--compress option specified but is ignored")
}
Expand Down Expand Up @@ -408,6 +416,7 @@ func GenBuildOptions(c *cobra.Command, inputArgs []string, iopts BuildOptions) (
GroupAdd: iopts.GroupAdd,
IDMappingOptions: idmappingOptions,
IIDFile: iopts.Iidfile,
BuildIDFile: iopts.BuildIDFile,
IgnoreFile: iopts.IgnoreFile,
In: stdin,
InheritLabels: inheritLabels,
Expand All @@ -417,6 +426,8 @@ func GenBuildOptions(c *cobra.Command, inputArgs []string, iopts BuildOptions) (
Labels: iopts.Label,
LayerLabels: iopts.LayerLabel,
Layers: layers,
CacheStages: iopts.CacheStages,
StageLabels: iopts.StageLabels,
LogFile: iopts.Logfile,
LogRusage: iopts.LogRusage,
LogSplitByPlatform: iopts.LogSplitByPlatform,
Expand Down
11 changes: 9 additions & 2 deletions pkg/cli/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ import (

// LayerResults represents the results of the layer flags
type LayerResults struct {
ForceRm bool
Layers bool
ForceRm bool
Layers bool
CacheStages bool
StageLabels bool
}

// UserNSResults represents the results for the UserNS flags
Expand Down Expand Up @@ -73,6 +75,7 @@ type BudResults struct {
Format string
From string
Iidfile string
BuildIDFile string
InheritLabels bool
InheritAnnotations bool
Label []string
Expand Down Expand Up @@ -217,6 +220,8 @@ func GetLayerFlags(flags *LayerResults) pflag.FlagSet {
fs := pflag.FlagSet{}
fs.BoolVar(&flags.ForceRm, "force-rm", false, "always remove intermediate containers after a build, even if the build is unsuccessful.")
fs.BoolVar(&flags.Layers, "layers", UseLayers(), "use intermediate layers during build. Use BUILDAH_LAYERS environment variable to override.")
fs.BoolVar(&flags.CacheStages, "cache-stages", false, "preserve intermediate stage images.")
fs.BoolVar(&flags.StageLabels, "stage-labels", false, "add metadata labels to intermediate stage images (requires --cache-stages).")
return fs
}

Expand Down Expand Up @@ -253,6 +258,7 @@ func GetBudFlags(flags *BudResults) pflag.FlagSet {
fs.StringSliceVarP(&flags.File, "file", "f", []string{}, "`pathname or URL` of a Dockerfile")
fs.StringVar(&flags.Format, "format", DefaultFormat(), "`format` of the built image's manifest and metadata. Use BUILDAH_FORMAT environment variable to override.")
fs.StringVar(&flags.Iidfile, "iidfile", "", "`file` to write the image ID to")
fs.StringVar(&flags.BuildIDFile, "build-id-file", "", "`file` to write the build ID to")
fs.IntVar(&flags.Jobs, "jobs", 1, "how many stages to run in parallel")
fs.StringArrayVar(&flags.Label, "label", []string{}, "set metadata for an image (default [])")
fs.StringArrayVar(&flags.LayerLabel, "layer-label", []string{}, "set metadata for an intermediate image (default [])")
Expand Down Expand Up @@ -357,6 +363,7 @@ func GetBudFlagsCompletions() commonComp.FlagCompletions {
flagCompletion["hooks-dir"] = commonComp.AutocompleteNone
flagCompletion["ignorefile"] = commonComp.AutocompleteDefault
flagCompletion["iidfile"] = commonComp.AutocompleteDefault
flagCompletion["build-id-file"] = commonComp.AutocompleteDefault
flagCompletion["jobs"] = commonComp.AutocompleteNone
flagCompletion["label"] = commonComp.AutocompleteNone
flagCompletion["layer-label"] = commonComp.AutocompleteNone
Expand Down
Loading