How to Set Container Resource Limits and ulimits Configuration in Apple Container
You configure container resource limits in Apple Container by passing the --ulimit flag with the format <type>=<soft>[:<hard>], which the runtime translates into POSIX rlimit structures applied when the process starts.
The Apple Container framework provides granular control over process resource consumption through ulimits configuration. By translating the --ulimit command-line flag into POSIX rlimit structures, the runtime enforces hard ceilings on file descriptors, memory, CPU time, and other kernel resources when spawning container processes. This implementation spans the CLI flag definition, parsing logic, and the data structures that the container runtime consumes.
Understanding the --ulimit Flag and CLI Syntax
The --ulimit option is declared in the CLI flag struct to accept multiple string values representing resource constraints.
In Sources/Services/ContainerAPIService/Client/Flags.swift (lines 91‑98), the flag is defined as:
@Option(name: .customLong("ulimit") … ) public var ulimits: [String] = []
When you invoke the container command, each --ulimit argument follows the pattern <type>=<soft>[:<hard>]. If you omit the hard limit, the runtime automatically sets it equal to the soft limit. For example, nofile=1024:2048 sets a soft limit of 1024 open files and a hard limit of 2048.
Parsing ulimits in the Source Code
When the command executes, the raw strings from flags.ulimits are handed to Parser.rlimits(_:) in Sources/Services/ContainerAPIService/Client/Parser.swift (lines 908‑921). This method iterates over the input array, parses each entry, and validates that no duplicate limit types exist.
public static func rlimits(_ rawUlimits: [String]) throws -> [ProcessConfiguration.Rlimit] {
var rlimits: [ProcessConfiguration.Rlimit] = []
rlimits.reserveCapacity(rawUlimits.count)
var seenTypes: Set<String> = []
for ulimit in rawUlimits {
let rlimit = try Parser.rlimit(ulimit) // parse a single entry
if seenTypes.contains(rlimit.limit) {
throw ContainerizationError(.invalidArgument,
message: "duplicate ulimit type: \(ulimit.split(separator: \"=\").first ?? \"\")")
}
seenTypes.insert(rlimit.limit)
rlimits.append(rlimit)
}
return rlimits
}
Mapping User-Friendly Names to Kernel Constants
The parser converts textual type names (like nofile or nproc) into kernel constants (like RLIMIT_NOFILE or RLIMIT_NPROC) using a static dictionary defined in Sources/Services/ContainerAPIService/Client/Parser.swift (lines 908‑925):
private static let ulimitNameToRlimit: [String: String] = [
"core": "RLIMIT_CORE",
"cpu": "RLIMIT_CPU",
"data": "RLIMIT_DATA",
"fsize": "RLIMIT_FSIZE",
"locks": "RLIMIT_LOCKS",
"memlock": "RLIMIT_MEMLOCK",
"msgqueue": "RLIMIT_MSGQUEUE",
"nice": "RLIMIT_NICE",
"nofile": "RLIMIT_NOFILE",
"nproc": "RLIMIT_NPROC",
"rss": "RLIMIT_RSS",
"rtprio": "RLIMIT_RTPRIO",
"rttime": "RLIMIT_RTTIME",
"sigpending":"RLIMIT_SIGPENDING",
"stack": "RLIMIT_STACK",
]
Validating Soft and Hard Limits
The single-entry parsing logic in Parser.rlimit(_:) (lines 953‑1014) splits each string on the = character, validates the format, and ensures the soft limit does not exceed the hard limit:
public static func rlimit(_ ulimit: String) throws -> ProcessConfiguration.Rlimit {
// <type>=<soft>[:<hard>]
let parts = ulimit.split(separator: "=", maxSplits: 1)
guard parts.count == 2 else {
throw ContainerizationError(.invalidArgument,
message: "invalid ulimit format '\(ulimit)': expected <type>=<soft>[:<hard>]")
}
let typeName = String(parts[0]).lowercased()
let valuesPart = String(parts[1])
// Map the textual name to the kernel constant
guard let rlimitType = ulimitNameToRlimit[typeName] else {
let validTypes = ulimitNameToRlimit.keys.sorted().joined(separator: ", ")
throw ContainerizationError(.invalidArgument,
message: "unsupported ulimit type '\(typeName)': valid types are \(validTypes)")
}
// Split soft/hard values
let valueParts = valuesPart.split(separator: ":", maxSplits: 1)
let soft: UInt64
let hard: UInt64
switch valueParts.count {
case 1:
soft = try parseRlimitValue(String(valueParts[0]), typeName: typeName)
hard = soft
case 2:
soft = try parseRlimitValue(String(valueParts[0]), typeName: typeName)
hard = try parseRlimitValue(String(valueParts[1]), typeName: typeName)
default:
throw ContainerizationError(.invalidArgument,
message: "invalid ulimit format '\(ulimit)': expected <type>=<soft>[:<hard>]")
}
// Validate soft ≤ hard
if soft > hard {
throw ContainerizationError(.invalidArgument,
message: "ulimit '\(typeName)' soft limit (\(soft)) cannot exceed hard limit (\(hard))")
}
return ProcessConfiguration.Rlimit(limit: rlimitType, soft: soft, hard: hard)
}
Storing Limits in ProcessConfiguration
Each parsed limit is stored as a ProcessConfiguration.Rlimit value. In Sources/ContainerResource/Container/ProcessConfiguration.swift (lines 34‑48), the structure is defined as:
public struct ProcessConfiguration: Sendable, Codable {
// ...
public var rlimits: [Rlimit]
// ...
public struct Rlimit: Sendable, Codable {
public let limit: String // e.g. "RLIMIT_NOFILE"
public let soft: UInt64
public let hard: UInt64
}
}
When the container launches, the runtime reads ProcessConfiguration.rlimits and invokes the appropriate system API (e.g., setrlimit(2)) for each entry, thereby enforcing the requested ceilings on file descriptors, processes, memory-lock size, and other resources.
Practical Implementation Examples
Command-Line Usage
Use the --ulimit flag multiple times to set different resource constraints:
# Limit the container to 1024 open files (soft) and 2048 (hard)
container run --ulimit nofile=1024:2048 -it ubuntu:latest
# Unlimited core dump size
container run --ulimit core=unlimited -it alpine:latest
# Set max number of processes to 512 (soft = hard)
container run --ulimit nproc=512 -d myimage
Programmatic Configuration in Swift
You can also construct ProcessConfiguration directly in Swift code:
import ContainerResource
// Build a ProcessConfiguration manually
let rlimit = ProcessConfiguration.Rlimit(
limit: "RLIMIT_NOFILE", // same keys used in the parser dictionary
soft: 1024,
hard: 2048
)
let cfg = ProcessConfiguration(
executable: "/bin/sh",
arguments: ["-c", "sleep 3600"],
environment: [],
workingDirectory: "/",
terminal: false,
user: .id(uid: 1000, gid: 1000),
supplementalGroups: [],
rlimits: [rlimit] // <- inject the limit
)
// Pass `cfg` to the Container API client (e.g. ContainerAPI.run(cfg))
Error Handling and Validation Rules
The parser enforces strict validation rules that surface as ContainerizationError(.invalidArgument, ...):
- Duplicate types: If you specify the same ulimit type twice (e.g.,
--ulimit nofile=1024 --ulimit nofile=2048), the runtime throws "duplicate ulimit type: nofile". - Unknown type: If you provide an unsupported type name, the error lists all valid types from the
ulimitNameToRlimitdictionary. - Soft limit exceeds hard limit: The runtime validates that the soft value is less than or equal to the hard value, throwing "soft limit cannot exceed hard limit" if violated.
- Invalid values: Non-numeric values (except the string "unlimited") trigger an error indicating the value must be a non-negative integer or "unlimited".
Handling Parsing Errors in Swift
do {
let limits = try Parser.rlimits(["nofile=1024:2048", "nproc=512"])
// pass `limits` to ProcessConfiguration
} catch let err as ContainerizationError {
// display a helpful message to the user
print("❗️ Invalid ulimit: \(err.message)")
}
Summary
- Container resource limits and ulimits configuration in Apple Container use the
--ulimitflag with the syntax<type>=<soft>[:<hard>]. - The CLI flag is defined in
Flags.swiftand parsed byParser.rlimits(_:)andParser.rlimit(_:)inParser.swift. - Textual type names are mapped to kernel constants (e.g.,
nofile→RLIMIT_NOFILE) before storage. - Limits are stored in the
ProcessConfiguration.Rlimitstruct and enforced by the runtime via system calls likesetrlimit(2). - The parser validates against duplicate types, unknown types, and soft limits exceeding hard limits, surfacing clear error messages for each violation.
Frequently Asked Questions
What is the correct syntax for setting container resource limits?
The correct syntax is <type>=<soft>[:<hard>], where type is a resource name like nofile or nproc, soft is the soft limit, and hard is the optional hard limit. If you omit the hard limit, the runtime sets it equal to the soft limit. You can also specify unlimited as the value for either limit.
Which ulimits types are supported by Apple Container?
The framework supports all standard POSIX rlimit types: core, cpu, data, fsize, locks, memlock, msgqueue, nice, nofile, nproc, rss, rtprio, rttime, sigpending, and stack. These are mapped to their respective RLIMIT_* constants in the kernel as defined in Parser.swift.
How does the runtime enforce ulimits after parsing?
After parsing, the runtime stores the limits in the ProcessConfiguration.Rlimit array. When the container process starts, the runtime iterates over this array and calls the appropriate system API (such as setrlimit(2)) to apply each limit to the process, enforcing the constraints at the kernel level.
What happens if I set a soft limit higher than the hard limit?
The parser in Parser.rlimit(_:) explicitly validates that the soft limit is less than or equal to the hard limit. If you violate this constraint (e.g., --ulimit nofile=2048:1024), the runtime throws a ContainerizationError with the message "ulimit 'nofile' soft limit (2048) cannot exceed hard limit (1024)".
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →