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 @@ -273,6 +273,8 @@ type BuildOptions struct {
IIDFile string
// IIDFileRaw tells the builder to write the image ID to the specified file without the algorithm prefix
IIDFileRaw 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 @@ -287,6 +289,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
// stage as its base). 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
56 changes: 56 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,21 @@ 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.

When `--cache-stages` is used, cache lookup is disabled to ensure a fresh build every time.
This means the build will not reuse cached intermediate images from previous builds. On the
other hand when `--cache-stages` is used with `--layers` in a first build, subsequent builds without
`--cache-stages` but with `--layers` can still use the preserved intermediate layers as cache.

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 @@ -1136,6 +1158,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 @@ -1468,6 +1505,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 .
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to show the bool values in play, perhaps

Suggested change
buildah build --cache-stages --stage-labels -t imageName .
buildah build --cache-stages false --stage-labels false -t imageName .

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I skipped that since this is a relatively uncommon use case for boolean flags, because the false behavior is implicit whenever the flag is absent. Other flags such as --no-cache or --layers follow the same pattern (present when true, absent when not, without presence of the --layers false) so I wanted to stay consistent with your docs. If you feel strongly about it, I certainly can add the examples :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a long-standing bit of confusion in the man pages where, even though the boolean argument values are optional, we don't suggest that they always have to be supplied in the --flag=value form, with an equal sign, to prevent them from being treated as unrelated arguments.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nalind Does it make a sense to you to add args with equal sign and boolean or leave it as it is? What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want an example where the flag is set to false, the equal sign is going to be necessary.


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 @@ -1530,6 +1571,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"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great! TY!

### 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.3
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
38 changes: 35 additions & 3 deletions imagebuildah/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,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 @@ -115,6 +116,10 @@ type executor struct {
layerLabels []string
annotations []string
layers bool
cacheStages bool
stageLabels bool
buildID 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 @@ -261,6 +266,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 @@ -316,6 +334,10 @@ func newExecutor(logger *logrus.Logger, logPrefix string, store storage.Store, o
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 @@ -856,6 +878,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 @@ -1054,9 +1085,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
53 changes: 53 additions & 0 deletions imagebuildah/stage_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,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 @@ -1254,6 +1255,11 @@ func (s *stageExecutor) execute(ctx context.Context, base string) (imgID string,
stage := s.stage
ib := stage.Builder
checkForLayers := s.executor.layers && s.executor.useCache
// When --cache-stages is used, disable cache lookup to ensure a fresh build.
// Subsequent builds without --cache-stages still can use these intermediate images as cache.
if s.executor.cacheStages {
checkForLayers = false
}
moreStages := s.index < len(s.stages)-1
lastStage := !moreStages
onlyBaseImage := false
Expand All @@ -1272,6 +1278,12 @@ 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 in stage labels when an
// intermediate stage uses another stage as its base.
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 @@ -1888,6 +1900,30 @@ 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 - only commit the final stage in such a 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)
// Determine if we need a new layer or just metadata:
// - If stage was already committed (imgID != ""), only add labels (emptyLayer=true)
// - If not yet committed (imgID == ""), capture filesystem changes (emptyLayer=false)
emptyLayer := imgID != ""
// Commit the stage without squashing, using empty output name (intermediate image)
imgID, commitResults, err = s.commit(ctx, createdBy, emptyLayer, "", 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 @@ -2599,6 +2635,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 (stages that serve as base).
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 this stage uses another stage as its base (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 @@ -223,6 +223,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 @@ -385,8 +393,10 @@ func GenBuildOptions(c *cobra.Command, inputArgs []string, iopts BuildOptions) (
Architecture: systemContext.ArchitectureChoice,
Args: args,
BlobDirectory: iopts.BlobCache,
BuildIDFile: iopts.BuildIDFile,
BuildOutputs: iopts.BuildOutputs,
CacheFrom: cacheFrom,
CacheStages: iopts.CacheStages,
CacheTo: cacheTo,
CacheTTL: cacheTTL,
CDIConfigDir: iopts.CDIConfigDir,
Expand Down Expand Up @@ -450,6 +460,7 @@ func GenBuildOptions(c *cobra.Command, inputArgs []string, iopts BuildOptions) (
SkipUnusedStages: skipUnusedStages,
SourceDateEpoch: sourceDateEpoch,
Squash: iopts.Squash,
StageLabels: iopts.StageLabels,
SystemContext: systemContext,
Target: iopts.Target,
Timestamp: timestamp,
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
CacheStages bool
ForceRm bool
Layers bool
StageLabels bool
}

// UserNSResults represents the results for the UserNS flags
Expand Down Expand Up @@ -59,6 +61,7 @@ type BudResults struct {
BuildArg []string
BuildArgFile []string
BuildContext []string
BuildIDFile string
CacheFrom []string
CacheTo []string
CacheTTL string
Expand Down Expand Up @@ -220,6 +223,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 @@ -257,6 +262,7 @@ func GetBudFlags(flags *BudResults) pflag.FlagSet {
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.IidfileRaw, "iidfile-raw", "", "`file` to write the image ID to (without algorithm prefix)")
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 @@ -364,6 +370,7 @@ func GetBudFlagsCompletions() commonComp.FlagCompletions {
flagCompletion["ignorefile"] = commonComp.AutocompleteDefault
flagCompletion["iidfile"] = commonComp.AutocompleteDefault
flagCompletion["iidfile-raw"] = commonComp.AutocompleteDefault
flagCompletion["build-id-file"] = commonComp.AutocompleteDefault
flagCompletion["jobs"] = commonComp.AutocompleteNone
flagCompletion["label"] = commonComp.AutocompleteNone
flagCompletion["layer-label"] = commonComp.AutocompleteNone
Expand Down
Loading