Merge pull request #8523 from dokku/3826-extending-resource-management-to-include-build-containers

Support resource limits on the build container
This commit is contained in:
Jose Diaz-Gonzalez
2026-04-29 10:45:16 -04:00
committed by GitHub
7 changed files with 285 additions and 6 deletions

View File

@@ -32,7 +32,7 @@ Valid resource options include:
See the [Supported Resource Management Properties](/docs/deployment/schedulers/docker-local.md#supported-resource-management-properties) section of the docker local scheduler documentation for more information on how each resource limit maps to Docker.
Resource limits and reservations are applied only during the `run` and `deploy` phases of an application, and will not impact the `build` phase of an application.
Resource reservations are applied only during the `run` and `deploy` phases of an application. Resource limits also apply during the `build` phase when explicitly configured against the `build` process type - see [Build-time Resource Limits](#build-time-resource-limits) below.
### Resource Limits
@@ -120,6 +120,40 @@ dokku resource:limit --process-type web node-js-app
nvidia-gpu:
```
#### Build-time Resource Limits
> [!IMPORTANT]
> New as of 0.38.0
Resource limits may be applied to the build container by using the special `build` process type. This allows constraining memory and CPU usage during the `build` phase, which is otherwise unconstrained.
```shell
dokku resource:limit --memory 4g --process-type build node-js-app
```
```
=====> Setting resource limits for node-js-app (build)
memory: 4g
```
Build-time limits are applied via the `docker-args-process-build` plugin trigger. They do not inherit from default (`_default_`) limits - only limits explicitly set against the `build` process type are applied at build time. This is intentional: build phases often require more memory than the runtime process, and silently inheriting a small runtime limit would cause confusing OOM failures.
> [!WARNING]
> Do not use `build` as a runtime process type in your `Procfile`. The `build` name is reserved by the resource plugin to scope limits to the build container, and using it as a Procfile entry will cause those limits to be applied to that runtime container as well.
Supported limits per builder:
| Builder | `--cpu` | `--memory` | `--memory-swap` | `--nvidia-gpu` |
|------------|---------|------------|-----------------|----------------|
| herokuish | yes | yes | yes | yes |
| dockerfile | no | yes | yes | no |
| pack | no | no | no | no |
| nixpacks | no | no | no | no |
| railpack | no | no | no | no |
| lambda | no | no | no | no |
Resource keys outside the supported set for a builder are silently ignored. Reservations (`resource:reserve`) are never applied at build time - only limits.
#### Clearing Resource Limits
In cases where the values are incorrect - or there is no desire to limit resources - resource limits may be cleared using the `resource:limit-clear` command.

View File

@@ -141,6 +141,28 @@ trigger-builder-dockerfile-builder-build() {
DOCKERFILE_ARGS+=("--label" "${1#--label=}")
shift 1
;;
--memory | -m)
DOCKERFILE_ARGS+=("--memory")
DOCKERFILE_ARGS+=("$2")
shift 2
;;
--memory=* | -m=*)
if [[ "$1" == "--memory=*" ]]; then
DOCKERFILE_ARGS+=("--memory" "${1#--memory=}")
elif [[ "$1" == "-m=*" ]]; then
DOCKERFILE_ARGS+=("--memory" "${1#-m=}")
fi
shift 1
;;
--memory-swap)
DOCKERFILE_ARGS+=("--memory-swap")
DOCKERFILE_ARGS+=("$2")
shift 2
;;
--memory-swap=*)
DOCKERFILE_ARGS+=("--memory-swap" "${1#--memory-swap=}")
shift 1
;;
--network)
DOCKERFILE_ARGS+=("--network")
DOCKERFILE_ARGS+=("$2")
@@ -205,7 +227,7 @@ trigger-builder-dockerfile-builder-build() {
shift 1
;;
--ssh)
DOCKERFILE_ARGS+=("--platform")
DOCKERFILE_ARGS+=("--ssh")
DOCKERFILE_ARGS+=("$2")
shift 2
;;
@@ -213,6 +235,15 @@ trigger-builder-dockerfile-builder-build() {
DOCKERFILE_ARGS+=("--ssh" "${1#--ssh=}")
shift 1
;;
--tag)
DOCKERFILE_ARGS+=("--tag")
DOCKERFILE_ARGS+=("$2")
shift 2
;;
--tag=*)
DOCKERFILE_ARGS+=("--tag" "${1#--tag=}")
shift 1
;;
--target)
DOCKERFILE_ARGS+=("--target")
DOCKERFILE_ARGS+=("$2")
@@ -223,12 +254,12 @@ trigger-builder-dockerfile-builder-build() {
shift 1
;;
--ulimit)
DOCKERFILE_ARGS+=("--tag")
DOCKERFILE_ARGS+=("--ulimit")
DOCKERFILE_ARGS+=("$2")
shift 2
;;
--tag=*)
DOCKERFILE_ARGS+=("--tag" "${1#--tag=}")
--ulimit=*)
DOCKERFILE_ARGS+=("--ulimit" "${1#--ulimit=}")
shift 1
;;
--check | -D | --debug | --no-cache)

View File

@@ -5,6 +5,7 @@
/install
/post-*
/report
/docker-args-process-build
/docker-args-process-deploy
/docker-args-process-run
/resource-get-property

View File

@@ -1,5 +1,5 @@
SUBCOMMANDS = subcommands/limit subcommands/limit-clear subcommands/report subcommands/reserve subcommands/reserve-clear
TRIGGERS = triggers/docker-args-process-deploy triggers/install triggers/post-app-clone-setup triggers/post-app-rename-setup triggers/post-delete triggers/report triggers/resource-get-property
TRIGGERS = triggers/docker-args-process-build triggers/docker-args-process-deploy triggers/install triggers/post-app-clone-setup triggers/post-app-rename-setup triggers/post-delete triggers/report triggers/resource-get-property
BUILD = commands subcommands triggers
PLUGIN_NAME = resource

View File

@@ -18,6 +18,10 @@ func main() {
var err error
switch trigger {
case "docker-args-process-build":
appName := flag.Arg(0)
builderType := flag.Arg(1)
err = resource.TriggerDockerArgsProcessBuild(appName, builderType)
case "docker-args-process-deploy":
appName := flag.Arg(0)
processType := flag.Arg(3)

View File

@@ -91,6 +91,84 @@ func TriggerDockerArgsProcessDeploy(appName string, processType string) error {
return nil
}
// TriggerDockerArgsProcessBuild outputs build-phase docker options for a builder
func TriggerDockerArgsProcessBuild(appName string, builderType string) error {
stdin, err := io.ReadAll(os.Stdin)
if err != nil {
return err
}
if os.Getenv("DOKKU_OMIT_RESOURCE_ARGS") == "1" {
fmt.Print(string(stdin))
return nil
}
allowed := validBuildLimitsForBuilder(builderType)
if len(allowed) == 0 {
fmt.Print(string(stdin))
return nil
}
resources, err := common.PropertyGetAll("resource", appName)
if err != nil {
fmt.Print(string(stdin))
return nil
}
limits := make(map[string]string)
prefix := "build.limit."
for key, value := range resources {
if !strings.HasPrefix(key, prefix) {
continue
}
resourceKey := strings.TrimPrefix(key, prefix)
if !allowed[resourceKey] {
continue
}
flagName := resourceKey
if resourceKey == "cpu" {
flagName = "cpus"
}
if resourceKey == "nvidia-gpu" {
flagName = "gpus"
}
limits[flagName] = value
}
for key, value := range limits {
if value == "" {
continue
}
value = addMemorySuffixForDocker(key, value)
fmt.Printf(" --%s=%s ", key, value)
}
fmt.Print(string(stdin))
return nil
}
// validBuildLimitsForBuilder returns the resource keys (in property-key form)
// that the named builder can apply during the build phase. An empty map means
// the builder does not support build-phase resource limits.
func validBuildLimitsForBuilder(builderType string) map[string]bool {
switch builderType {
case "herokuish":
return map[string]bool{
"cpu": true,
"memory": true,
"memory-swap": true,
"nvidia-gpu": true,
}
case "dockerfile":
return map[string]bool{
"memory": true,
"memory-swap": true,
}
}
return nil
}
// TriggerInstall runs the install step for the resource plugin
func TriggerInstall() error {
if err := common.PropertySetup("resource"); err != nil {

131
tests/unit/resource_3.bats Normal file
View File

@@ -0,0 +1,131 @@
#!/usr/bin/env bats
load test_helper
setup() {
global_setup
create_app
}
teardown() {
destroy_app
global_teardown
}
@test "(resource) resource:limit --process-type build (herokuish)" {
run /bin/bash -c "dokku resource:limit --memory 256m --cpu 1 --memory-swap 512m --nvidia-gpu 1 --process-type build $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku resource:report --resource-build.limit.memory $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_output "256m"
run /bin/bash -c "PLUGIN_PATH=$PLUGIN_PATH PLUGIN_CORE_AVAILABLE_PATH=$PLUGIN_CORE_AVAILABLE_PATH DOKKU_LIB_ROOT=$DOKKU_LIB_ROOT plugn trigger docker-args-process-build $TEST_APP herokuish < /dev/null"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "--memory=256m"
assert_output_contains "--cpus=1"
assert_output_contains "--memory-swap=512m"
assert_output_contains "--gpus=1"
}
@test "(resource) resource:limit --process-type build (dockerfile filters cpu and gpu)" {
run /bin/bash -c "dokku resource:limit --memory 512m --cpu 1 --nvidia-gpu 1 --memory-swap 1g --process-type build $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "PLUGIN_PATH=$PLUGIN_PATH PLUGIN_CORE_AVAILABLE_PATH=$PLUGIN_CORE_AVAILABLE_PATH DOKKU_LIB_ROOT=$DOKKU_LIB_ROOT plugn trigger docker-args-process-build $TEST_APP dockerfile < /dev/null"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "--memory=512m"
assert_output_contains "--memory-swap=1g"
assert_output_not_contains "--cpus="
assert_output_not_contains "--gpus="
}
@test "(resource) resource:limit --process-type build (pack/lambda emit nothing)" {
run /bin/bash -c "dokku resource:limit --memory 512m --process-type build $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "PLUGIN_PATH=$PLUGIN_PATH PLUGIN_CORE_AVAILABLE_PATH=$PLUGIN_CORE_AVAILABLE_PATH DOKKU_LIB_ROOT=$DOKKU_LIB_ROOT plugn trigger docker-args-process-build $TEST_APP pack < /dev/null"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_not_contains "--memory"
run /bin/bash -c "PLUGIN_PATH=$PLUGIN_PATH PLUGIN_CORE_AVAILABLE_PATH=$PLUGIN_CORE_AVAILABLE_PATH DOKKU_LIB_ROOT=$DOKKU_LIB_ROOT plugn trigger docker-args-process-build $TEST_APP lambda < /dev/null"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_not_contains "--memory"
}
@test "(resource) resource:limit defaults do not leak into build trigger" {
run /bin/bash -c "dokku resource:limit --memory 128m $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "PLUGIN_PATH=$PLUGIN_PATH PLUGIN_CORE_AVAILABLE_PATH=$PLUGIN_CORE_AVAILABLE_PATH DOKKU_LIB_ROOT=$DOKKU_LIB_ROOT plugn trigger docker-args-process-build $TEST_APP herokuish < /dev/null"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_not_contains "--memory"
}
@test "(resource) resource:limit reservations do not apply at build time" {
run /bin/bash -c "dokku resource:reserve --memory 128m --process-type build $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "PLUGIN_PATH=$PLUGIN_PATH PLUGIN_CORE_AVAILABLE_PATH=$PLUGIN_CORE_AVAILABLE_PATH DOKKU_LIB_ROOT=$DOKKU_LIB_ROOT plugn trigger docker-args-process-build $TEST_APP herokuish < /dev/null"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_not_contains "--memory-reservation"
assert_output_not_contains "--memory"
}
@test "(resource) DOKKU_OMIT_RESOURCE_ARGS suppresses build trigger" {
run /bin/bash -c "dokku resource:limit --memory 256m --process-type build $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "PLUGIN_PATH=$PLUGIN_PATH PLUGIN_CORE_AVAILABLE_PATH=$PLUGIN_CORE_AVAILABLE_PATH DOKKU_LIB_ROOT=$DOKKU_LIB_ROOT DOKKU_OMIT_RESOURCE_ARGS=1 plugn trigger docker-args-process-build $TEST_APP herokuish < /dev/null"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_not_contains "--memory"
}
@test "(resource) resource:limit-clear --process-type build" {
run /bin/bash -c "dokku resource:limit --memory 256m --process-type build $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku resource:report --resource-build.limit.memory $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_output "256m"
run /bin/bash -c "dokku resource:limit-clear --process-type build $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku resource:report --resource-build.limit.memory $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_failure
}