build: Add per-step CPU and memory resource limits

Port of moby/buildkit#6569 to buildx. Adds --memory, --memory-swap,
--cpu-shares, --cpu-period, --cpu-quota, --cpuset-cpus, and --cpuset-mems
flags to build, plus the equivalent bake target attributes and compose
x-bake fields.

Signed-off-by: Jiří Moravčík <jiri.moravcik@gmail.com>
This commit is contained in:
Jiří Moravčík
2026-06-08 15:39:14 +02:00
parent 4fbc1e5f2c
commit 80b2293881
14 changed files with 761 additions and 48 deletions
+53 -26
View File
@@ -606,6 +606,13 @@ func (c Config) newOverrides(v []string) (map[string]map[string]Override, error)
override.Append = appendTo
override.ArrValue = append(override.ArrValue, parts[1])
}
case "resources":
if len(keys) != 3 {
return nil, errors.Errorf("invalid key %s, resources requires name", parts[0])
}
if len(parts) == 2 {
override.Value = parts[1]
}
case "args":
if len(keys) != 3 {
return nil, errors.Errorf("invalid key %s, args requires name", parts[0])
@@ -745,32 +752,33 @@ type Target struct {
// Inherits is the only field that cannot be overridden with --set
Inherits []string `json:"inherits,omitempty" hcl:"inherits,optional" cty:"inherits"`
Annotations []string `json:"annotations,omitempty" hcl:"annotations,optional" cty:"annotations"`
Attest buildflags.Attests `json:"attest,omitempty" hcl:"attest,optional" cty:"attest"`
Context *string `json:"context,omitempty" hcl:"context,optional" cty:"context"`
Contexts map[string]string `json:"contexts,omitempty" hcl:"contexts,optional" cty:"contexts"`
Dockerfile *string `json:"dockerfile,omitempty" hcl:"dockerfile,optional" cty:"dockerfile"`
DockerfileInline *string `json:"dockerfile-inline,omitempty" hcl:"dockerfile-inline,optional" cty:"dockerfile-inline"`
Args map[string]*string `json:"args,omitempty" hcl:"args,optional" cty:"args"`
Labels map[string]*string `json:"labels,omitempty" hcl:"labels,optional" cty:"labels"`
Tags []string `json:"tags,omitempty" hcl:"tags,optional" cty:"tags"`
CacheFrom buildflags.CacheOptions `json:"cache-from,omitempty" hcl:"cache-from,optional" cty:"cache-from"`
CacheTo buildflags.CacheOptions `json:"cache-to,omitempty" hcl:"cache-to,optional" cty:"cache-to"`
Target *string `json:"target,omitempty" hcl:"target,optional" cty:"target"`
Secrets buildflags.Secrets `json:"secret,omitempty" hcl:"secret,optional" cty:"secret"`
SSH buildflags.SSHKeys `json:"ssh,omitempty" hcl:"ssh,optional" cty:"ssh"`
Platforms []string `json:"platforms,omitempty" hcl:"platforms,optional" cty:"platforms"`
Outputs buildflags.Exports `json:"output,omitempty" hcl:"output,optional" cty:"output"`
Pull *bool `json:"pull,omitempty" hcl:"pull,optional" cty:"pull"`
NoCache *bool `json:"no-cache,omitempty" hcl:"no-cache,optional" cty:"no-cache"`
NetworkMode *string `json:"network,omitempty" hcl:"network,optional" cty:"network"`
NoCacheFilter []string `json:"no-cache-filter,omitempty" hcl:"no-cache-filter,optional" cty:"no-cache-filter"`
ShmSize *string `json:"shm-size,omitempty" hcl:"shm-size,optional" cty:"shm-size"`
Ulimits []string `json:"ulimits,omitempty" hcl:"ulimits,optional" cty:"ulimits"`
Call *string `json:"call,omitempty" hcl:"call,optional" cty:"call"`
Entitlements []string `json:"entitlements,omitempty" hcl:"entitlements,optional" cty:"entitlements"`
ExtraHosts map[string]*string `json:"extra-hosts,omitempty" hcl:"extra-hosts,optional" cty:"extra-hosts"`
Policy buildflags.PolicyConfigs `json:"policy,omitempty" hcl:"policy,optional" cty:"policy"`
Annotations []string `json:"annotations,omitempty" hcl:"annotations,optional" cty:"annotations"`
Attest buildflags.Attests `json:"attest,omitempty" hcl:"attest,optional" cty:"attest"`
Context *string `json:"context,omitempty" hcl:"context,optional" cty:"context"`
Contexts map[string]string `json:"contexts,omitempty" hcl:"contexts,optional" cty:"contexts"`
Dockerfile *string `json:"dockerfile,omitempty" hcl:"dockerfile,optional" cty:"dockerfile"`
DockerfileInline *string `json:"dockerfile-inline,omitempty" hcl:"dockerfile-inline,optional" cty:"dockerfile-inline"`
Args map[string]*string `json:"args,omitempty" hcl:"args,optional" cty:"args"`
Labels map[string]*string `json:"labels,omitempty" hcl:"labels,optional" cty:"labels"`
Tags []string `json:"tags,omitempty" hcl:"tags,optional" cty:"tags"`
CacheFrom buildflags.CacheOptions `json:"cache-from,omitempty" hcl:"cache-from,optional" cty:"cache-from"`
CacheTo buildflags.CacheOptions `json:"cache-to,omitempty" hcl:"cache-to,optional" cty:"cache-to"`
Target *string `json:"target,omitempty" hcl:"target,optional" cty:"target"`
Secrets buildflags.Secrets `json:"secret,omitempty" hcl:"secret,optional" cty:"secret"`
SSH buildflags.SSHKeys `json:"ssh,omitempty" hcl:"ssh,optional" cty:"ssh"`
Platforms []string `json:"platforms,omitempty" hcl:"platforms,optional" cty:"platforms"`
Outputs buildflags.Exports `json:"output,omitempty" hcl:"output,optional" cty:"output"`
Pull *bool `json:"pull,omitempty" hcl:"pull,optional" cty:"pull"`
NoCache *bool `json:"no-cache,omitempty" hcl:"no-cache,optional" cty:"no-cache"`
NetworkMode *string `json:"network,omitempty" hcl:"network,optional" cty:"network"`
NoCacheFilter []string `json:"no-cache-filter,omitempty" hcl:"no-cache-filter,optional" cty:"no-cache-filter"`
ShmSize *string `json:"shm-size,omitempty" hcl:"shm-size,optional" cty:"shm-size"`
Ulimits []string `json:"ulimits,omitempty" hcl:"ulimits,optional" cty:"ulimits"`
Resources *buildflags.ResourcesConfig `json:"resources,omitempty" hcl:"resources,optional" cty:"resources"`
Call *string `json:"call,omitempty" hcl:"call,optional" cty:"call"`
Entitlements []string `json:"entitlements,omitempty" hcl:"entitlements,optional" cty:"entitlements"`
ExtraHosts map[string]*string `json:"extra-hosts,omitempty" hcl:"extra-hosts,optional" cty:"extra-hosts"`
Policy buildflags.PolicyConfigs `json:"policy,omitempty" hcl:"policy,optional" cty:"policy"`
// IMPORTANT: if you add more fields here, do not forget to update newOverrides/AddOverrides and docs/bake-reference.md.
// linked is a private field to mark a target used as a linked one
@@ -944,6 +952,9 @@ func (t *Target) Merge(t2 *Target) {
if t2.Ulimits != nil { // merge
t.Ulimits = append(t.Ulimits, t2.Ulimits...)
}
if t2.Resources != nil { // merge
t.Resources = t.Resources.Merge(t2.Resources)
}
if t2.Description != "" {
t.Description = t2.Description
}
@@ -1138,6 +1149,16 @@ func (t *Target) AddOverrides(overrides map[string]Override, ent *EntitlementCon
} else {
t.Ulimits = o.ArrValue
}
case "resources":
if len(keys) != 2 {
return errors.Errorf("invalid format for resources, expecting resources.<name>=<value>")
}
if t.Resources == nil {
t.Resources = &buildflags.ResourcesConfig{}
}
if err := t.Resources.SetField(keys[1], value); err != nil {
return err
}
case "network":
t.NetworkMode = &value
case "pull":
@@ -1612,6 +1633,12 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
}
bo.Ulimits = ulimits
resourceLimits, err := build.ParseResourceLimits(t.Resources.ToEntries())
if err != nil {
return nil, err
}
bo.ResourceLimits = resourceLimits
bo.Allow = append(bo.Allow, t.Entitlements...)
return bo, nil
+126
View File
@@ -27,6 +27,15 @@ target "webDEP" {
no-cache = true
shm-size = "128m"
ulimits = ["nofile=1024:1024"]
resources = {
memory = "2g"
memory-swap = "4g"
cpu-shares = 1024
cpu-period = 100000
cpu-quota = 50000
cpuset-cpus = "0-3"
cpuset-mems = "0,1"
}
extra-hosts = {
my_hostname = "8.8.8.8"
}
@@ -67,6 +76,14 @@ target "webapp" {
require.Equal(t, true, *m["webapp"].NoCache)
require.Equal(t, "128m", *m["webapp"].ShmSize)
require.Equal(t, []string{"nofile=1024:1024"}, m["webapp"].Ulimits)
require.NotNil(t, m["webapp"].Resources)
require.Equal(t, ptrstr("2g"), m["webapp"].Resources.Memory)
require.Equal(t, ptrstr("4g"), m["webapp"].Resources.MemorySwap)
require.Equal(t, int64(1024), *m["webapp"].Resources.CPUShares)
require.Equal(t, int64(100000), *m["webapp"].Resources.CPUPeriod)
require.Equal(t, int64(50000), *m["webapp"].Resources.CPUQuota)
require.Equal(t, ptrstr("0-3"), m["webapp"].Resources.CPUSetCPUs)
require.Equal(t, ptrstr("0,1"), m["webapp"].Resources.CPUSetMems)
require.Equal(t, map[string]*string{"my_hostname": ptrstr("8.8.8.8")}, m["webapp"].ExtraHosts)
require.Nil(t, m["webapp"].Pull)
@@ -226,6 +243,21 @@ target "webapp" {
require.Equal(t, "256m", *m["webapp"].ShmSize)
})
t.Run("ResourceLimitsOverride", func(t *testing.T) {
m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{
"webapp.resources.memory=512m",
"webapp.resources.cpu-quota=25000",
}, nil, nil, &EntitlementConf{})
require.NoError(t, err)
require.Equal(t, ptrstr("512m"), m["webapp"].Resources.Memory)
require.Equal(t, int64(25000), *m["webapp"].Resources.CPUQuota)
})
t.Run("ResourceLimitsInvalidOverride", func(t *testing.T) {
_, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.resources.cpu-quota=notanumber"}, nil, nil, &EntitlementConf{})
require.Error(t, err)
})
t.Run("PullOverride", func(t *testing.T) {
t.Parallel()
m, g, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.pull=false"}, nil, nil, &EntitlementConf{})
@@ -1834,6 +1866,100 @@ func TestExtraHostsDeterministicOrder(t *testing.T) {
}
}
func TestResourceLimitsToBuildOpt(t *testing.T) {
newInt64 := func(v int64) *int64 { return &v }
bo, err := toBuildOpt(&Target{
DockerfileInline: ptrstr("FROM scratch"),
Resources: &buildflags.ResourcesConfig{
Memory: ptrstr("2g"),
MemorySwap: ptrstr("4g"),
CPUShares: newInt64(1024),
CPUPeriod: newInt64(100000),
CPUQuota: newInt64(50000),
CPUSetCPUs: ptrstr("0-3"),
CPUSetMems: ptrstr("0,1"),
},
}, &Input{})
require.NoError(t, err)
require.Equal(t, int64(2*1024*1024*1024), bo.ResourceLimits.Memory.Value())
require.Equal(t, int64(4*1024*1024*1024), bo.ResourceLimits.MemorySwap.Value())
require.Equal(t, int64(1024), bo.ResourceLimits.CPUShares)
require.Equal(t, int64(100000), bo.ResourceLimits.CPUPeriod)
require.Equal(t, int64(50000), bo.ResourceLimits.CPUQuota)
require.Equal(t, "0-3", bo.ResourceLimits.CPUSetCPUs)
require.Equal(t, "0,1", bo.ResourceLimits.CPUSetMems)
}
func TestResourceLimitsUnlimitedSwap(t *testing.T) {
bo, err := toBuildOpt(&Target{
DockerfileInline: ptrstr("FROM scratch"),
Resources: &buildflags.ResourcesConfig{MemorySwap: ptrstr("-1")},
}, &Input{})
require.NoError(t, err)
require.Equal(t, int64(-1), bo.ResourceLimits.MemorySwap.Value())
}
func TestResourceLimitsInvalid(t *testing.T) {
_, err := toBuildOpt(&Target{
DockerfileInline: ptrstr("FROM scratch"),
Resources: &buildflags.ResourcesConfig{Memory: ptrstr("notabyte")},
}, &Input{})
require.Error(t, err)
}
func TestResourcesObject(t *testing.T) {
ctx := context.TODO()
cpuShares, cpuQuota := int64(1024), int64(50000)
want := &buildflags.ResourcesConfig{
Memory: ptrstr("2g"),
MemorySwap: ptrstr("4g"),
CPUShares: &cpuShares,
CPUQuota: &cpuQuota,
CPUSetCPUs: ptrstr("0-3"),
}
data := `target "app" {
resources = {
memory = "2g"
memory-swap = "4g"
cpu-shares = 1024
cpu-quota = 50000
cpuset-cpus = "0-3"
}
}`
m, _, err := ReadTargets(ctx, []File{{Name: "docker-bake.hcl", Data: []byte(data)}}, []string{"app"}, nil, nil, nil, &EntitlementConf{})
require.NoError(t, err)
require.Equal(t, want, m["app"].Resources)
}
// TestResourcesInheritIsolation ensures merging resources from a shared base
// target doesn't leak a child's override into the base or a sibling.
func TestResourcesInheritIsolation(t *testing.T) {
ctx := context.TODO()
data := `target "base" {
resources = {
memory = "1g"
}
}
target "child1" {
inherits = ["base"]
resources = {
memory = "2g"
}
}
target "child2" {
inherits = ["base"]
}`
m, _, err := ReadTargets(ctx, []File{{Name: "docker-bake.hcl", Data: []byte(data)}}, []string{"child1", "child2"}, nil, nil, nil, &EntitlementConf{})
require.NoError(t, err)
require.Equal(t, ptrstr("2g"), m["child1"].Resources.Memory)
require.Equal(t, ptrstr("1g"), m["child2"].Resources.Memory)
}
func TestAnnotations(t *testing.T) {
fp := File{
Name: "docker-bake.hcl",
+13
View File
@@ -87,6 +87,7 @@ type Options struct {
Tags []string
Target string
Ulimits *opts.UlimitOpt
ResourceLimits ResourceLimits
Session []session.Attachable
Linked bool // Linked marks this target as exclusively linked (not requested by the user).
@@ -98,6 +99,18 @@ type Options struct {
Policy []buildflags.PolicyConfig
}
// ResourceLimits holds the cgroup resource constraints applied to individual
// build steps (RUN instructions). They don't affect the build cache key.
type ResourceLimits struct {
Memory opts.MemBytes
MemorySwap opts.MemSwapBytes
CPUShares int64
CPUPeriod int64
CPUQuota int64
CPUSetCPUs string
CPUSetMems string
}
type CallFunc struct {
Name string
Format string
+3
View File
@@ -610,6 +610,9 @@ func toSolveOpt(ctx context.Context, np *noderesolver.ResolvedNode, multiDriver
so.FrontendAttrs["ulimit"] = ulimits
}
// setup per-step resource limits
addResourceLimits(opt.ResourceLimits, so.FrontendAttrs)
// mark call request as internal
if opt.CallFunc != nil {
so.Internal = true
+91
View File
@@ -99,6 +99,97 @@ func toBuildkitUlimits(inp *opts.UlimitOpt) (string, error) {
return strings.Join(ulimits, ","), nil
}
// User-facing resource keys accepted in `--resource key=value` entries, mirroring docker run flag names.
const (
resourceKeyMemory = "memory"
resourceKeyMemorySwap = "memory-swap"
resourceKeyCPUShares = "cpu-shares"
resourceKeyCPUPeriod = "cpu-period"
resourceKeyCPUQuota = "cpu-quota"
resourceKeyCPUSetCPUs = "cpuset-cpus"
resourceKeyCPUSetMems = "cpuset-mems"
)
// Frontend attribute keys, must match those parsed by BuildKit's dockerui frontend.
const (
attrMemory = "memory"
attrMemorySwap = "memswap"
attrCPUShares = "cpushares"
attrCPUPeriod = "cpuperiod"
attrCPUQuota = "cpuquota"
attrCPUSetCPUs = "cpusetcpus"
attrCPUSetMems = "cpusetmems"
)
// ParseResourceLimits parses `key=value` entries from the `--resource` flag into ResourceLimits.
func ParseResourceLimits(entries []string) (ResourceLimits, error) {
var rl ResourceLimits
for _, entry := range entries {
k, v, ok := strings.Cut(entry, "=")
if !ok {
return rl, errors.Errorf("invalid resource %q, expected key=value", entry)
}
k = strings.TrimSpace(k)
v = strings.TrimSpace(v)
switch k {
case resourceKeyMemory:
if err := rl.Memory.Set(v); err != nil {
return rl, errors.Wrapf(err, "invalid value %q for resource %s", v, k)
}
case resourceKeyMemorySwap:
if err := rl.MemorySwap.Set(v); err != nil {
return rl, errors.Wrapf(err, "invalid value %q for resource %s", v, k)
}
case resourceKeyCPUShares, resourceKeyCPUPeriod, resourceKeyCPUQuota:
n, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return rl, errors.Wrapf(err, "invalid value %q for resource %s", v, k)
}
switch k {
case resourceKeyCPUShares:
rl.CPUShares = n
case resourceKeyCPUPeriod:
rl.CPUPeriod = n
case resourceKeyCPUQuota:
rl.CPUQuota = n
}
case resourceKeyCPUSetCPUs:
rl.CPUSetCPUs = v
case resourceKeyCPUSetMems:
rl.CPUSetMems = v
default:
return rl, errors.Errorf("unknown resource %q", k)
}
}
return rl, nil
}
// addResourceLimits sets the frontend attributes for the resource limits.
// Only non-zero values are sent, so builds against a daemon without the feature keep working.
func addResourceLimits(rl ResourceLimits, attrs map[string]string) {
if v := rl.Memory.Value(); v > 0 {
attrs[attrMemory] = strconv.FormatInt(v, 10)
}
if v := rl.MemorySwap.Value(); v != 0 {
attrs[attrMemorySwap] = strconv.FormatInt(v, 10)
}
if rl.CPUShares > 0 {
attrs[attrCPUShares] = strconv.FormatInt(rl.CPUShares, 10)
}
if rl.CPUPeriod > 0 {
attrs[attrCPUPeriod] = strconv.FormatInt(rl.CPUPeriod, 10)
}
if rl.CPUQuota > 0 {
attrs[attrCPUQuota] = strconv.FormatInt(rl.CPUQuota, 10)
}
if rl.CPUSetCPUs != "" {
attrs[attrCPUSetCPUs] = rl.CPUSetCPUs
}
if rl.CPUSetMems != "" {
attrs[attrCPUSetMems] = rl.CPUSetMems
}
}
func notSupported(f driver.Feature, d *driver.DriverHandle, docs string) error {
return errors.Errorf(`%s is not supported for the %s driver.
Switch to a different driver, or turn on the containerd image store, and try again.
+107
View File
@@ -5,6 +5,7 @@ import (
"strings"
"testing"
dockeropts "github.com/docker/cli/opts"
"github.com/stretchr/testify/require"
)
@@ -150,3 +151,109 @@ func TestToBuildkitExtraHosts(t *testing.T) {
})
}
}
func TestAddResourceLimits(t *testing.T) {
mustMemSwap := func(v string) dockeropts.MemSwapBytes {
var m dockeropts.MemSwapBytes
require.NoError(t, m.Set(v))
return m
}
tests := []struct {
name string
limits ResourceLimits
expected map[string]string
}{
{
name: "empty",
limits: ResourceLimits{},
expected: map[string]string{},
},
{
name: "all",
limits: ResourceLimits{
Memory: dockeropts.MemBytes(2 * 1024 * 1024 * 1024),
MemorySwap: mustMemSwap("4g"),
CPUShares: 1024,
CPUPeriod: 100000,
CPUQuota: 50000,
CPUSetCPUs: "0-3",
CPUSetMems: "0,1",
},
expected: map[string]string{
"memory": "2147483648",
"memswap": "4294967296",
"cpushares": "1024",
"cpuperiod": "100000",
"cpuquota": "50000",
"cpusetcpus": "0-3",
"cpusetmems": "0,1",
},
},
{
name: "unlimited swap",
limits: ResourceLimits{MemorySwap: mustMemSwap("-1")},
expected: map[string]string{
"memswap": "-1",
},
},
{
name: "partial",
limits: ResourceLimits{Memory: dockeropts.MemBytes(512 * 1024 * 1024)},
expected: map[string]string{
"memory": "536870912",
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
attrs := map[string]string{}
addResourceLimits(tc.limits, attrs)
require.Equal(t, tc.expected, attrs)
})
}
}
func TestParseResourceLimits(t *testing.T) {
t.Run("all", func(t *testing.T) {
rl, err := ParseResourceLimits([]string{
"memory=2g",
"memory-swap=4g",
"cpu-shares=1024",
"cpu-period=100000",
"cpu-quota=50000",
"cpuset-cpus=0-3",
"cpuset-mems=0,1",
})
require.NoError(t, err)
require.Equal(t, int64(2*1024*1024*1024), rl.Memory.Value())
require.Equal(t, int64(4*1024*1024*1024), rl.MemorySwap.Value())
require.Equal(t, int64(1024), rl.CPUShares)
require.Equal(t, int64(100000), rl.CPUPeriod)
require.Equal(t, int64(50000), rl.CPUQuota)
require.Equal(t, "0-3", rl.CPUSetCPUs)
require.Equal(t, "0,1", rl.CPUSetMems)
})
t.Run("unlimited swap", func(t *testing.T) {
rl, err := ParseResourceLimits([]string{"memory-swap=-1"})
require.NoError(t, err)
require.Equal(t, int64(-1), rl.MemorySwap.Value())
})
t.Run("missing value", func(t *testing.T) {
_, err := ParseResourceLimits([]string{"memory"})
require.Error(t, err)
})
t.Run("unknown key", func(t *testing.T) {
_, err := ParseResourceLimits([]string{"bogus=1"})
require.Error(t, err)
})
t.Run("invalid int", func(t *testing.T) {
_, err := ParseResourceLimits([]string{"cpu-shares=notanumber"})
require.Error(t, err)
})
}
+51 -22
View File
@@ -89,6 +89,11 @@ type buildOptions struct {
target string
ulimits *dockeropts.UlimitOpt
resources []string
// legacyResources is kept separate from resources because the pflag
// StringArray for --resource resets its backing slice on first use.
legacyResources []string
attests []string
sbom string
provenance string
@@ -133,6 +138,7 @@ func (o *buildOptions) toOptions() (*BuildOptions, error) {
Tags: o.tags,
Target: o.target,
Ulimits: o.ulimits,
Resources: append(o.legacyResources, o.resources...),
Builder: o.builder,
NoCache: o.noCache,
Pull: o.pull,
@@ -234,6 +240,20 @@ func (o *buildOptions) toDisplayMode() (progressui.DisplayMode, error) {
return progress, nil
}
// legacyResourceValue is a pflag.Value that appends a "<key>=value" entry to
// the --resource list, wiring the hidden legacy flags into the same code path.
type legacyResourceValue struct {
key string
resources *[]string
}
func (v *legacyResourceValue) String() string { return "" }
func (v *legacyResourceValue) Type() string { return "string" }
func (v *legacyResourceValue) Set(s string) error {
*v.resources = append(*v.resources, v.key+"="+s)
return nil
}
const (
commandNameAttribute = attribute.Key("command.name")
commandOptionsHash = attribute.Key("command.options.hash")
@@ -583,6 +603,8 @@ func buildCmd(dockerCli command.Cli, rootOpts *rootOptions, debugger debuggerOpt
options.ulimits = dockeropts.NewUlimitOpt(nil)
flags.Var(options.ulimits, "ulimit", "Ulimit options")
flags.StringArrayVar(&options.resources, "resource", []string{}, `Resource limits for build containers (format: "memory=2g", "cpu-quota=50000")`)
flags.StringArrayVar(&options.attests, "attest", []string{}, `Attestation parameters (format: "type=sbom,generator=image")`)
flags.StringVar(&options.sbom, "sbom", "", `Shorthand for "--attest=type=sbom"`)
flags.StringVar(&options.provenance, "provenance", "", `Shorthand for "--attest=type=provenance"`)
@@ -595,12 +617,33 @@ func buildCmd(dockerCli command.Cli, rootOpts *rootOptions, debugger debuggerOpt
var ignore string
var ignoreSlice []string
var ignoreBool bool
var ignoreInt int64
flags.StringVar(&options.callFunc, "print", "", "Print result of information request (e.g., outline, targets)")
cobrautil.MarkFlagsExperimental(flags, "print")
flags.MarkHidden("print")
// Legacy per-resource flags, hidden and superseded by --resource.
flags.VarP(&legacyResourceValue{key: "memory", resources: &options.legacyResources}, "memory", "m", "Memory limit")
flags.MarkHidden("memory")
flags.VarP(&legacyResourceValue{key: "memory-swap", resources: &options.legacyResources}, "memory-swap", "", `Swap limit equal to memory plus swap: "-1" to enable unlimited swap`)
flags.MarkHidden("memory-swap")
flags.VarP(&legacyResourceValue{key: "cpu-shares", resources: &options.legacyResources}, "cpu-shares", "c", "CPU shares (relative weight)")
flags.MarkHidden("cpu-shares")
flags.VarP(&legacyResourceValue{key: "cpu-period", resources: &options.legacyResources}, "cpu-period", "", "Limit the CPU CFS (Completely Fair Scheduler) period")
flags.MarkHidden("cpu-period")
flags.VarP(&legacyResourceValue{key: "cpu-quota", resources: &options.legacyResources}, "cpu-quota", "", "Limit the CPU CFS (Completely Fair Scheduler) quota")
flags.MarkHidden("cpu-quota")
flags.VarP(&legacyResourceValue{key: "cpuset-cpus", resources: &options.legacyResources}, "cpuset-cpus", "", `CPUs in which to allow execution ("0-3", "0,1")`)
flags.MarkHidden("cpuset-cpus")
flags.VarP(&legacyResourceValue{key: "cpuset-mems", resources: &options.legacyResources}, "cpuset-mems", "", `MEMs in which to allow execution ("0-3", "0,1")`)
flags.MarkHidden("cpuset-mems")
flags.BoolVar(&ignoreBool, "compress", false, "Compress the build context using gzip")
flags.MarkHidden("compress")
@@ -617,27 +660,6 @@ func buildCmd(dockerCli command.Cli, rootOpts *rootOptions, debugger debuggerOpt
flags.SetAnnotation("squash", "flag-warn", []string{"experimental flag squash is removed with BuildKit. You should squash inside build using a multi-stage Dockerfile for efficiency."})
cobrautil.MarkFlagsExperimental(flags, "squash")
flags.StringVarP(&ignore, "memory", "m", "", "Memory limit")
flags.MarkHidden("memory")
flags.StringVar(&ignore, "memory-swap", "", `Swap limit equal to memory plus swap: "-1" to enable unlimited swap`)
flags.MarkHidden("memory-swap")
flags.Int64VarP(&ignoreInt, "cpu-shares", "c", 0, "CPU shares (relative weight)")
flags.MarkHidden("cpu-shares")
flags.Int64Var(&ignoreInt, "cpu-period", 0, "Limit the CPU CFS (Completely Fair Scheduler) period")
flags.MarkHidden("cpu-period")
flags.Int64Var(&ignoreInt, "cpu-quota", 0, "Limit the CPU CFS (Completely Fair Scheduler) quota")
flags.MarkHidden("cpu-quota")
flags.StringVar(&ignore, "cpuset-cpus", "", `CPUs in which to allow execution ("0-3", "0,1")`)
flags.MarkHidden("cpuset-cpus")
flags.StringVar(&ignore, "cpuset-mems", "", `MEMs in which to allow execution ("0-3", "0,1")`)
flags.MarkHidden("cpuset-mems")
flags.BoolVar(&ignoreBool, "rm", true, "Remove intermediate containers after a successful build")
flags.MarkHidden("rm")
@@ -990,6 +1012,7 @@ type BuildOptions struct {
Tags []string
Target string
Ulimits *dockeropts.UlimitOpt
Resources []string
Builder string
NoCache bool
Pull bool
@@ -1014,6 +1037,11 @@ func RunBuild(ctx context.Context, dockerCli command.Cli, in *BuildOptions, inSt
contexts[name] = build.NamedContext{Path: path}
}
resourceLimits, err := build.ParseResourceLimits(in.Resources)
if err != nil {
return nil, nil, err
}
opts := build.Options{
Inputs: build.Inputs{
ContextPath: in.ContextPath,
@@ -1034,6 +1062,7 @@ func RunBuild(ctx context.Context, dockerCli command.Cli, in *BuildOptions, inSt
Tags: in.Tags,
Target: in.Target,
Ulimits: in.Ulimits,
ResourceLimits: resourceLimits,
GroupRef: in.GroupRef,
ProvenanceResponseMode: confutil.ParseMetadataProvenance(in.ProvenanceResponseMode),
}
+35
View File
@@ -110,6 +110,13 @@ type configOutput struct {
ShmSize string `json:",omitempty"`
Ulimit string `json:",omitempty"`
Memory string `json:",omitempty"`
MemorySwap string `json:",omitempty"`
CPUShares string `json:",omitempty"`
CPUPeriod string `json:",omitempty"`
CPUQuota string `json:",omitempty"`
CPUSetCPUs string `json:",omitempty"`
CPUSetMems string `json:",omitempty"`
CacheMountNS string `json:",omitempty"`
DockerfileCheckConfig string `json:",omitempty"`
SourceDateEpoch string `json:",omitempty"`
@@ -391,6 +398,13 @@ workers0:
readAttr(attrs, "shm-size", &out.Config.ShmSize, nil)
readAttr(attrs, "ulimit", &out.Config.Ulimit, nil)
readAttr(attrs, "memory", &out.Config.Memory, nil)
readAttr(attrs, "memswap", &out.Config.MemorySwap, nil)
readAttr(attrs, "cpushares", &out.Config.CPUShares, nil)
readAttr(attrs, "cpuperiod", &out.Config.CPUPeriod, nil)
readAttr(attrs, "cpuquota", &out.Config.CPUQuota, nil)
readAttr(attrs, "cpusetcpus", &out.Config.CPUSetCPUs, nil)
readAttr(attrs, "cpusetmems", &out.Config.CPUSetMems, nil)
readAttr(attrs, "build-arg:BUILDKIT_CACHE_MOUNT_NS", &out.Config.CacheMountNS, nil)
readAttr(attrs, "build-arg:BUILDKIT_DOCKERFILE_CHECK", &out.Config.DockerfileCheckConfig, nil)
readAttr(attrs, "build-arg:"+epoch.SourceDateEpochEnv, &out.Config.SourceDateEpoch, nil)
@@ -570,6 +584,27 @@ workers0:
if out.Config.Ulimit != "" {
fmt.Fprintf(tw, "Resource Limits:\t%s\n", out.Config.Ulimit)
}
if out.Config.Memory != "" {
fmt.Fprintf(tw, "Memory:\t%s\n", out.Config.Memory)
}
if out.Config.MemorySwap != "" {
fmt.Fprintf(tw, "Memory Swap:\t%s\n", out.Config.MemorySwap)
}
if out.Config.CPUShares != "" {
fmt.Fprintf(tw, "CPU Shares:\t%s\n", out.Config.CPUShares)
}
if out.Config.CPUPeriod != "" {
fmt.Fprintf(tw, "CPU Period:\t%s\n", out.Config.CPUPeriod)
}
if out.Config.CPUQuota != "" {
fmt.Fprintf(tw, "CPU Quota:\t%s\n", out.Config.CPUQuota)
}
if out.Config.CPUSetCPUs != "" {
fmt.Fprintf(tw, "CPUSet CPUs:\t%s\n", out.Config.CPUSetCPUs)
}
if out.Config.CPUSetMems != "" {
fmt.Fprintf(tw, "CPUSet Mems:\t%s\n", out.Config.CPUSetMems)
}
if out.Config.CacheMountNS != "" {
fmt.Fprintf(tw, "Cache Mount Namespace:\t%s\n", out.Config.CacheMountNS)
}
+24
View File
@@ -239,6 +239,7 @@ The following table shows the complete list of attributes that you can assign to
| [`policy`](#targetpolicy) | List | Policies to validate build sources and metadata |
| [`platforms`](#targetplatforms) | List | Target platforms |
| [`pull`](#targetpull) | Boolean | Always pull images |
| [`resources`](#targetresources) | Map | Resource limits for build containers |
| [`secret`](#targetsecret) | List | Secrets to expose to the build |
| [`shm-size`](#targetshm-size) | List | Size of `/dev/shm` |
| [`ssh`](#targetssh) | List | SSH agent sockets or keys to expose to the build |
@@ -939,6 +940,29 @@ target "default" {
}
```
### `target.resources`
Sets cgroup resource limits for the containers that run `RUN` instructions
during the build. The supported keys are `memory`, `memory-swap`, `cpu-shares`,
`cpu-period`, `cpu-quota`, `cpuset-cpus`, and `cpuset-mems`. These map to the
equivalent `docker build` flags and to the
[`--resource`](https://docs.docker.com/reference/cli/docker/buildx/build/#resource)
flag for `docker buildx build`.
```hcl
target "default" {
resources = {
memory = "2g"
memory-swap = "4g"
cpu-quota = 50000
}
}
```
> [!NOTE]
> These limits require a BuildKit daemon that supports per-step resource limits
> and only take effect on Linux. They don't affect the build cache key.
### `target.secret`
Defines secrets to expose to the build target.
+38
View File
@@ -44,6 +44,7 @@ Start a build
| `--pull` | `bool` | | Always attempt to pull all referenced images |
| [`--push`](#push) | `bool` | | Shorthand for `--output=type=registry,unpack=false` |
| `-q`, `--quiet` | `bool` | | Suppress the build output and print image ID on success |
| [`--resource`](#resource) | `stringArray` | | Resource limits for build containers (format: `memory=2g`, `cpu-quota=50000`) |
| [`--sbom`](#sbom) | `string` | | Shorthand for `--attest=type=sbom` |
| [`--secret`](#secret) | `stringArray` | | Secret to expose to the build (format: `id=mysecret[,src=/local/secret]`) |
| [`--shm-size`](#shm-size) | `bytes` | `0` | Shared memory size for build containers |
@@ -1158,3 +1159,40 @@ $ docker buildx build --ulimit nofile=1024:1024 .
> In most cases, it is recommended to let the builder automatically determine
> the appropriate configurations. Manual adjustments should only be considered
> when specific performance tuning is required for complex build scenarios.
### <a name="resource"></a> Set CPU and memory limits for build containers (--resource)
The `--resource` flag constrains the resources available to the containers that
run your `RUN` instructions during the build. It's repeatable and takes
`key=value` pairs, where `key` is one of:
| Key | Description |
|:--------------|:-----------------------------------------------------------------------------|
| `memory` | Memory limit (format: `<number><unit>`, e.g. `512m`, `2g`). |
| `memory-swap` | Total memory plus swap limit. Set to `-1` to allow unlimited swap. |
| `cpu-shares` | CPU shares (relative weight). |
| `cpu-period` | Length of a CPU CFS (Completely Fair Scheduler) period, in microseconds. |
| `cpu-quota` | CPU CFS quota, in microseconds, within each `cpu-period`. |
| `cpuset-cpus` | CPUs in which to allow execution (`0-3`, `0,1`). |
| `cpuset-mems` | Memory nodes (MEMs) in which to allow execution (`0-3`, `0,1`). |
```console
$ docker buildx build --resource memory=2g --resource cpu-quota=50000 --resource cpu-period=100000 .
```
These map to the cgroup resource limits of the legacy `docker build` API and
only apply to individual build steps. They don't affect the build cache key.
> [!NOTE]
> These limits require a BuildKit daemon that supports per-step resource limits
> (the `exec.meta.linux.resources` capability) and only take effect on Linux.
> [!NOTE]
> Because BuildKit can run build steps in parallel, these limits apply to each
> step in isolation rather than to the build as a whole. When the same step is
> requested with different limits, the most relaxed limits are used.
> [!NOTE]
> In most cases, it is recommended to let the builder automatically determine
> the appropriate configurations. Manual adjustments should only be considered
> when specific performance tuning is required for complex build scenarios.
+1
View File
@@ -36,6 +36,7 @@ Start a build
| `--pull` | `bool` | | Always attempt to pull all referenced images |
| `--push` | `bool` | | Shorthand for `--output=type=registry,unpack=false` |
| `-q`, `--quiet` | `bool` | | Suppress the build output and print image ID on success |
| `--resource` | `stringArray` | | Resource limits for build containers (format: `memory=2g`, `cpu-quota=50000`) |
| `--sbom` | `string` | | Shorthand for `--attest=type=sbom` |
| `--secret` | `stringArray` | | Secret to expose to the build (format: `id=mysecret[,src=/local/secret]`) |
| `--shm-size` | `bytes` | `0` | Shared memory size for build containers |
+1
View File
@@ -40,6 +40,7 @@ Start a build
| `--pull` | `bool` | | Always attempt to pull all referenced images |
| `--push` | `bool` | | Shorthand for `--output=type=registry,unpack=false` |
| `-q`, `--quiet` | `bool` | | Suppress the build output and print image ID on success |
| `--resource` | `stringArray` | | Resource limits for build containers (format: `memory=2g`, `cpu-quota=50000`) |
| `--sbom` | `string` | | Shorthand for `--attest=type=sbom` |
| `--secret` | `stringArray` | | Secret to expose to the build (format: `id=mysecret[,src=/local/secret]`) |
| `--shm-size` | `bytes` | `0` | Shared memory size for build containers |
+110
View File
@@ -0,0 +1,110 @@
package buildflags
import (
"strconv"
"github.com/pkg/errors"
)
// ResourcesConfig holds the cgroup resource limits applied to individual build
// steps (RUN instructions). It maps to the `--resource` build flags.
type ResourcesConfig struct {
Memory *string `json:"memory,omitempty"`
MemorySwap *string `json:"memory-swap,omitempty"`
CPUShares *int64 `json:"cpu-shares,omitempty"`
CPUPeriod *int64 `json:"cpu-period,omitempty"`
CPUQuota *int64 `json:"cpu-quota,omitempty"`
CPUSetCPUs *string `json:"cpuset-cpus,omitempty"`
CPUSetMems *string `json:"cpuset-mems,omitempty"`
}
func (r *ResourcesConfig) Merge(other *ResourcesConfig) *ResourcesConfig {
if r == nil {
r = &ResourcesConfig{}
}
merged := *r
if other != nil {
if other.Memory != nil {
merged.Memory = other.Memory
}
if other.MemorySwap != nil {
merged.MemorySwap = other.MemorySwap
}
if other.CPUShares != nil {
merged.CPUShares = other.CPUShares
}
if other.CPUPeriod != nil {
merged.CPUPeriod = other.CPUPeriod
}
if other.CPUQuota != nil {
merged.CPUQuota = other.CPUQuota
}
if other.CPUSetCPUs != nil {
merged.CPUSetCPUs = other.CPUSetCPUs
}
if other.CPUSetMems != nil {
merged.CPUSetMems = other.CPUSetMems
}
}
return &merged
}
// ToEntries renders the config as `key=value` entries for build.ParseResourceLimits.
func (r *ResourcesConfig) ToEntries() []string {
if r == nil {
return nil
}
var entries []string
if r.Memory != nil {
entries = append(entries, "memory="+*r.Memory)
}
if r.MemorySwap != nil {
entries = append(entries, "memory-swap="+*r.MemorySwap)
}
if r.CPUShares != nil {
entries = append(entries, "cpu-shares="+strconv.FormatInt(*r.CPUShares, 10))
}
if r.CPUPeriod != nil {
entries = append(entries, "cpu-period="+strconv.FormatInt(*r.CPUPeriod, 10))
}
if r.CPUQuota != nil {
entries = append(entries, "cpu-quota="+strconv.FormatInt(*r.CPUQuota, 10))
}
if r.CPUSetCPUs != nil {
entries = append(entries, "cpuset-cpus="+*r.CPUSetCPUs)
}
if r.CPUSetMems != nil {
entries = append(entries, "cpuset-mems="+*r.CPUSetMems)
}
return entries
}
// SetField sets a single resource field by name, used by bake `--set` overrides.
func (r *ResourcesConfig) SetField(name, value string) error {
switch name {
case "memory":
r.Memory = &value
case "memory-swap":
r.MemorySwap = &value
case "cpu-shares", "cpu-period", "cpu-quota":
n, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return errors.Errorf("invalid value %s for int64 key resources.%s", value, name)
}
switch name {
case "cpu-shares":
r.CPUShares = &n
case "cpu-period":
r.CPUPeriod = &n
case "cpu-quota":
r.CPUQuota = &n
}
case "cpuset-cpus":
r.CPUSetCPUs = &value
case "cpuset-mems":
r.CPUSetMems = &value
default:
return errors.Errorf("unknown resources key %s", name)
}
return nil
}
+108
View File
@@ -0,0 +1,108 @@
package buildflags
import (
"sync"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-cty/cty/gocty"
)
var resourcesType = sync.OnceValue(func() cty.Type {
return cty.ObjectWithOptionalAttrs(
map[string]cty.Type{
"memory": cty.String,
"memory-swap": cty.String,
"cpu-shares": cty.Number,
"cpu-period": cty.Number,
"cpu-quota": cty.Number,
"cpuset-cpus": cty.String,
"cpuset-mems": cty.String,
},
[]string{"memory", "memory-swap", "cpu-shares", "cpu-period", "cpu-quota", "cpuset-cpus", "cpuset-mems"},
)
})
func (r *ResourcesConfig) FromCtyValue(in cty.Value, p cty.Path) error {
conv, err := convert.Convert(in, resourcesType())
if err != nil {
return p.NewError(err)
}
if v := conv.GetAttr("memory"); !v.IsNull() && v.IsKnown() {
s := v.AsString()
r.Memory = &s
}
if v := conv.GetAttr("memory-swap"); !v.IsNull() && v.IsKnown() {
s := v.AsString()
r.MemorySwap = &s
}
if v := conv.GetAttr("cpu-shares"); !v.IsNull() && v.IsKnown() {
var n int64
if err := gocty.FromCtyValue(v, &n); err != nil {
return p.NewError(err)
}
r.CPUShares = &n
}
if v := conv.GetAttr("cpu-period"); !v.IsNull() && v.IsKnown() {
var n int64
if err := gocty.FromCtyValue(v, &n); err != nil {
return p.NewError(err)
}
r.CPUPeriod = &n
}
if v := conv.GetAttr("cpu-quota"); !v.IsNull() && v.IsKnown() {
var n int64
if err := gocty.FromCtyValue(v, &n); err != nil {
return p.NewError(err)
}
r.CPUQuota = &n
}
if v := conv.GetAttr("cpuset-cpus"); !v.IsNull() && v.IsKnown() {
s := v.AsString()
r.CPUSetCPUs = &s
}
if v := conv.GetAttr("cpuset-mems"); !v.IsNull() && v.IsKnown() {
s := v.AsString()
r.CPUSetMems = &s
}
return nil
}
func (r *ResourcesConfig) ToCtyValue() cty.Value {
if r == nil {
return cty.NullVal(resourcesType())
}
vals := map[string]cty.Value{
"memory": cty.NullVal(cty.String),
"memory-swap": cty.NullVal(cty.String),
"cpu-shares": cty.NullVal(cty.Number),
"cpu-period": cty.NullVal(cty.Number),
"cpu-quota": cty.NullVal(cty.Number),
"cpuset-cpus": cty.NullVal(cty.String),
"cpuset-mems": cty.NullVal(cty.String),
}
if r.Memory != nil {
vals["memory"] = cty.StringVal(*r.Memory)
}
if r.MemorySwap != nil {
vals["memory-swap"] = cty.StringVal(*r.MemorySwap)
}
if r.CPUShares != nil {
vals["cpu-shares"] = cty.NumberIntVal(*r.CPUShares)
}
if r.CPUPeriod != nil {
vals["cpu-period"] = cty.NumberIntVal(*r.CPUPeriod)
}
if r.CPUQuota != nil {
vals["cpu-quota"] = cty.NumberIntVal(*r.CPUQuota)
}
if r.CPUSetCPUs != nil {
vals["cpuset-cpus"] = cty.StringVal(*r.CPUSetCPUs)
}
if r.CPUSetMems != nil {
vals["cpuset-mems"] = cty.StringVal(*r.CPUSetMems)
}
return cty.ObjectVal(vals)
}