Deploy resources — cpu, memory, pids
A node running several spaces shares one kernel and one pool of RAM. Without a cap, the service that leaks decides how much everyone else gets. `resources:` puts a ceiling on each container.
For AI coding agents (Claude Code, Codex, Cursor)
This page is the source of truth for the
resources:block in adeploy.yaml. It maps toResourcesSpecinspacemanager/resources.go, which parses intoServiceResourcesand reaches the container as cgroups v2 limits viaspace/cgroups.go.Hard rules: the four keys are
cpu,memory,pids,require— nothing else parses. Every field is optional. A service that names nopidscap still gets the default fork-bomb backstop. Applying caps rootless requires cgroups v2 delegation; without it a cap is silently dropped unless the service setsrequire: true, which turns the same situation into a launch failure.
Why the block exists
A space is an isolation boundary for networking and storage, not for compute. Every container on a node draws from the same CPU and the same RAM. A service that leaks memory is therefore not just its own problem: the kernel OOM killer picks a victim by score, and the victim is frequently not the service that caused the pressure. One leaking container can take down unrelated spaces on the same node.
resources: closes that gap by giving each container its own cgroups v2 limits, so the blast radius of a runaway service is the service itself.
Shape
services:
api:
image: rg.fr-par.scw.cloud/skalpai/api:20260824-1200-a1b2c3d
resources:
cpu: 1.5
memory: 512Mi
pids: 2048
require: true
The same block is accepted on a jobs: entry.
| Key | Type | Meaning |
|---|---|---|
cpu | string | Core count. 1.5 and 1500m are the same cap. Maps to cpu.max (quota over a fixed 100ms period). |
memory | string | Hard ceiling. Maps to memory.max. Exceeding it gets the container OOM-killed — the container, not the node. |
pids | int | Max processes/threads. Maps to pids.max. Defaults to a backstop when unset. |
require | bool | Fail the launch when the caps cannot be enforced, instead of running uncapped. |
Quantities
cpu accepts a plain core count (2, 0.5, 1.5) or the milli-core form (2000m, 500m, 1500m). Both spell the same limit.
memory accepts a raw byte count (536870912) or a suffixed quantity. Binary suffixes are powers of 1024 (Ki, Mi, Gi, Ti); decimal ones are powers of 1000 (KB/K, MB/M, GB/G, TB/T). So 512Mi is 536,870,912 bytes and 512MB is 512,000,000 — prefer the binary form for RAM.
An unparsable quantity fails at parse time, naming the service, rather than at launch.
The pids default
Every deployed service gets pids.max = 4096 when its config names no pids cap — including a service with no resources: block at all. It is a fork-bomb backstop: a runaway fork() loop exhausts the kernel's process table and makes a node unusable for every workload on it, not just the guilty one.
This one applies more widely than the others. The pids controller is normally delegated even on hosts where cpu and memory are not, so the backstop holds where a memory cap could not.
Rootless delegation, and what require is for
Applying cgroup limits without root needs cgroups v2 with a delegated subtree — in practice Delegate=cpu memory pids on the operator's user@.service. When that is missing, sklp cannot enforce what the YAML asked for. There are only two honest options, and the config picks one:
- default (
requireabsent or false) — best-effort. The cap is dropped and the service runs unbounded. Right for a dev machine, where a missing cap should not stop you from working. require: true— the cap is a contract. The launch fails with the reason (which controller is missing) rather than starting a service you believe is bounded when it is not.
On a production node, prefer require: true. A cap you think you have is worse than no cap: it is the one you will not think to check when the node falls over.
To enable delegation:
# /etc/systemd/system/user@.service.d/delegate.conf
[Service]
Delegate=cpu cpuset io memory pids
Then systemctl daemon-reload and re-login so the user slice picks it up. sudo sklet host setup writes this drop-in for you and reports which controllers the slice still lacks.
Root without systemd (a VM guest, a CI container) needs no delegation: there is nothing to delegate from. sklp enables the controllers it needs under /sys/fs/cgroup/sklp and runc writes the limits there directly (the cgroupfs driver). The require semantics are unchanged.
CI runs
CI spaces do not read this block: their runner is capped by the spawner's CPU budget (SKLP_CI_CPUS, default max(2, NumCPU/4)), applied as cpu.max plus a pids backstop, and in CI with require semantics. See CI — CPU budget.
What this is not
There is no requests:, and no scheduler. sklet applies the deploy contract it is handed; it does not sum the declared caps against the node's capacity and refuse to place a service that would oversubscribe it. Not oversubscribing the node is the operator's call, expressed in the YAML.
That is deliberate: admission control only starts paying for itself once there are several nodes to choose between. With one VM, a limit is the useful half — it bounds the damage — and a request would only be bookkeeping.