ADR-0015: customizations.kubernetes for pod resources and node placement¶
Status: accepted Date: 2026-10-07
Builds on ADR-0014, which still
applies to repositories that don't use customizations.kubernetes.
Context¶
hostRequirements (ADR-0014) is
the only way a repository can size its workspace, and it can't say what a
Kubernetes workspace often needs:
- A request apart from its limit.
cpus: 4reserves 4 cores; a repository can't ask for "1 core reserved, burst to 4", or for a memory limit equal to its request (Guaranteed QoS). - Other resources.
ephemeral-storage, a GPU count other than one, or any extended resource a device plugin exposes (nvidia.com/gpu: 2,example.com/fpga). - Node placement. GPU or other dedicated nodes are usually tainted and
labelled. A pod reaches them only with a
nodeSelectorand tolerations, anddevcontainer.jsonhas nothing that maps to either.
The Dev Container spec's extension point is customizations.<tool>. Each
tool owns its namespace. devcontainer-builder already exposes the image's
merged configuration to the template as the data source's
configuration_json, where customizations.<tool> is a list: one entry
per contributor (base image metadata, then each Feature, then
devcontainer.json), in that order. Reading it needs no service or
provider change.
Coder's own Dev Containers integration uses customizations.coder, so that
key is taken.
Decision¶
The template reads customizations.kubernetes:
"customizations": {
"kubernetes": {
"resources": {
"requests": { "cpu": "2", "memory": "4Gi", "ephemeral-storage": "10Gi" },
"limits": { "memory": "8Gi", "nvidia.com/gpu": "1" }
},
"storage": "20Gi",
"nodeSelector": { "nvidia.com/gpu.present": "true" },
"tolerations": [ { "key": "nvidia.com/gpu", "operator": "Exists", "effect": "NoSchedule" } ]
}
}
- Merge. Entries merge in contribution order.
requests,limitsandnodeSelectormerge per key, with the later entry winning.storageis the last one set.tolerationsare appended, and identical ones are kept once. So a Feature can carry defaults that the repository's owndevcontainer.jsonoverrides. resourcesis authoritative when it sets anything. If the mergedresourceshas any valid request or limit, it decides the pod's CPU and memory.hostRequirements.cpus/.memory(andrunArgs --cpus/--memory) are then ignored, and the build log says which values were ignored. We don't combine the two: picking the larger would make a request belowhostRequirementsimpossible, and that's the point of usingresources.- Requests are used as given.
cpuandmemoryare still capped bymax_cpu/max_memory, with a warning, as ADR-0014's are. Other names are passed through. Whenresourcessets no CPU or memory request, the default (250m / 512Mi) applies, lowered to the limit if the limit is smaller. - Limits are used as given, and are not capped: they reserve nothing.
For
cpu/memorywithout a limit, the limit is the larger of the request and the CPU/Memory parameter, as in ADR-0014. A limit below its request (a pod Kubernetes would reject) is raised to the request, with a warning. - Extended resources and hugepages (anything but
cpu,memory,ephemeral-storage) can't be overcommitted: Kubernetes requires request == limit and defaults the request to the limit. They're set as limits only, using the limit if given and else the request. A request that differs from the limit gets a warning. - GPU.
hostRequirements.gpustill meansnvidia.com/gpu: 1, but anvidia.com/gpuinresourceswins. storagereplaceshostRequirements.storagewhen it's set. It's a minimum, likehostRequirements.storage: the volume is the larger of it and the Disk parameter, decided when the workspace is created.- Node placement is opt-in.
nodeSelectorandtolerationsapply only with the new template variableallow_node_placement(defaultfalse). Placement is an operator decision: taints exist to keep workloads off nodes. With the variable off, the build log says they were ignored. With it on, the template's own selector keys (kubernetes.io/arch) always win. - Invalid input never fails the plan. Quantities are parsed in
Terraform locals ("500m", "1.5", "4Gi", "1G", plain numbers; no exponent
form). Resource names must be
cpu,memory,ephemeral-storage,hugepages-<size>or<domain>/<name>. Tolerations must be shaped the way Kubernetes accepts them. Anything else is dropped with a build-log warning, so a typo costs a warning and not a workspace that won't start. - The Resources (reserved / limit) metadata item names the source:
from customizations.kubernetes,from hostRequirements, orfrom the parameters(both, whenstorageandresourcescome from different places). It also lists the other resources the pod got.
Consequences¶
- Repositories can describe a Kubernetes workspace in Kubernetes terms: separate requests and limits, Guaranteed QoS, any number of GPUs, ephemeral storage. The same file still works in VS Code Dev Containers and Codespaces, which ignore a customization namespace they don't own.
- A Feature can package a team's sizing and placement defaults.
- Operators keep their levers.
max_cpu/max_memorycap what a repository can reserve, andallow_node_placementdecides whether repositories may reach tainted or labelled nodes. Limits and extended resources aren't capped. A repository asking for GPUs the cluster lacks stays Pending. - The rules live in Terraform locals (quantity parsing, merging, validation), so they're harder to unit-test than service code. They were checked with a scratch module that runs the template's locals against sample configurations.
customizations.kubernetesis this project's own schema. It isn't part of the Dev Container spec or of any other tool, and changing it later is a breaking change for repositories that use it.