How to Gracefully Stop a Container with a Custom Timeout in Apple Container
Use the container stop command with the --timeout flag to specify a custom grace period in seconds, or construct a ContainerStopOptions struct with timeoutInSeconds when calling the Swift API to ensure clean shutdowns before force termination.
The apple/container repository provides a robust runtime for managing container lifecycles, including a configurable graceful shutdown mechanism. When you need to stop a running container, the runtime allows you to specify exactly how long to wait for the process to exit cleanly before sending a force-kill signal. This capability ensures that applications have sufficient time to flush state, complete in-flight requests, and release resources properly.
Understanding the Graceful Stop Mechanism
The graceful stop implementation in RuntimeService.swift (lines 508-540) follows a predictable state machine that balances courtesy with enforcement. When you initiate a stop request, the runtime transitions the container to the stopping state, delivers your chosen signal (defaulting to SIGTERM), and waits for the specified duration.
If the container exits within the timeout window, the state transitions immediately to stopped. However, if the process ignores the signal and continues running past the deadline, the runtime automatically escalates to SIGKILL to guarantee termination. This two-phase approach prevents hung containers while respecting application cleanup needs.
Using the CLI to Stop Containers with Custom Timeouts
The command-line interface provides straightforward flags for controlling shutdown behavior. The --timeout parameter accepts an integer representing seconds, while --signal optionally specifies which POSIX signal to send initially.
Stop a single container with a 15-second grace period:
container stop my-app --timeout 15
Specify both a custom signal and extended timeout:
container stop my-app --timeout 30 --signal SIGTERM
Key defaults to remember:
- Default timeout: 10 seconds if
--timeoutis omitted - Default signal:
SIGTERMif--signalis omitted - Per-container application: When using
--all, the timeout applies independently to each container
The CLI implementation in StopCommand.swift parses these flags, constructs a ContainerStopOptions value, and forwards the request via RuntimeClient.swift (lines 181-192) to the runtime service over XPC.
Implementing Graceful Stops in Swift
For programmatic control, import the ContainerResource and RuntimeClient modules to build stop requests directly.
Constructing ContainerStopOptions
The ContainerStopOptions struct, defined in ContainerStopOptions.swift, encapsulates the optional signal name and timeout duration:
import ContainerResource
let stopOpts = ContainerStopOptions(
signal: "SIGTERM",
timeoutInSeconds: 15
)
Both parameters are optional; omitting them defaults to the runtime's standard values.
Calling RuntimeClient.stop
Use the RuntimeClient to send the stop request asynchronously:
import RuntimeClient
let client = RuntimeClient(containerID: "my-app")
let opts = ContainerStopOptions(signal: "SIGTERM", timeoutInSeconds: 20)
Task {
do {
try await client.stop(options: opts)
print("Container stopped gracefully")
} catch {
print("Failed to stop container: \(error)")
}
}
The stop method packages the options into an XPC message and awaits the runtime's response, throwing errors if the container does not exist or if a stop-then-remove race condition occurs (as tested in TestCLIRmRace.swift).
How the Runtime Handles Stop Requests
According to the implementation in RuntimeService.swift, the stop logic executes atomically to prevent state corruption:
- State transition: Sets internal state to
.stopping - Signal delivery: Sends the requested signal (e.g.,
SIGTERM) to the container's PID - Timer activation: Waits for the specified
timeoutInSecondsduration - Conditional escalation: If state remains
.stoppingafter timeout, sendsSIGKILL - Finalization: Transitions state to
.stoppedand returns success
This implementation ensures that the operation is idempotent—stopping an already-stopped container returns success without error, simplifying error handling in orchestration scripts.
Summary
- Custom timeouts prevent data loss by giving containers time to shut down cleanly while preventing indefinite hangs
- CLI usage:
container stop <id> --timeout <seconds>with optional--signalflag - Swift API: Construct
ContainerStopOptionsand callRuntimeClient.stop(options:) - Core implementation: Located in
RuntimeService.swift(lines 508-540) with state machine management - Safety guarantees: Automatic
SIGKILLescalation after timeout and idempotent operation design
Frequently Asked Questions
What is the default timeout when stopping a container?
The default timeout is 10 seconds if you do not specify the --timeout flag in the CLI or omit the timeoutInSeconds parameter in the ContainerStopOptions struct. This provides a reasonable balance for most applications while preventing excessive wait times.
Can I use a custom signal instead of SIGTERM?
Yes. Both the CLI --signal flag and the ContainerStopOptions initializer accept custom signal names such as SIGINT, SIGUSR1, or SIGTERM. The runtime parses the signal string and delivers it to the container's main process before beginning the timeout countdown.
What happens if the container ignores the stop signal?
If the container process does not exit within the specified timeout period, the runtime automatically sends SIGKILL to force immediate termination. This escalation happens transparently in RuntimeService.swift, ensuring the container reaches the .stopped state even when the application is unresponsive.
Is the stop operation idempotent?
Yes. The stop operation is fully idempotent according to the test suite in TestCLIStop.swift. Calling stop on an already-stopped container returns success without modifying state or returning an error, making it safe to use in retry loops and orchestration workflows.
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 →