How to Gracefully Stop a Container with a Custom Timeout in apple/container
Use the container stop command with the --timeout flag or instantiate ContainerStopOptions with timeoutInSeconds to send SIGTERM, wait for your specified duration, then automatically force-kill the container if it hasn't exited.
The apple/container repository provides a robust runtime for managing container lifecycles with fine-grained control over shutdown behavior. Gracefully stopping a container with a custom timeout ensures that applications have sufficient time to flush state, close connections, and perform cleanup operations before the system terminates the process. This mechanism is implemented through the ContainerStopOptions structure and coordinated between the CLI, RuntimeClient, and RuntimeService via XPC communication.
Understanding the Stop Mechanism
The graceful stop implementation in apple/container follows a strict state machine that transitions containers from running to stopped while respecting your specified timeout. When you initiate a stop command, the runtime first moves the container to a stopping state, sends the configured signal, and waits for either the process to exit or the timeout to elapse.
ContainerStopOptions Structure
The ContainerStopOptions struct defined in ContainerStopOptions.swift encapsulates the two configurable parameters for container shutdown:
- signal: An optional
Stringrepresenting the Unix signal to send (defaults toSIGTERMif not specified) - timeoutInSeconds: An
Intrepresenting the grace period before forceful termination (defaults to 10 seconds)
Here is how the options are structured in the source:
import ContainerResource
let stopOpts = ContainerStopOptions(signal: "SIGTERM", timeoutInSeconds: 15)
State Transitions and Signal Handling
According to the implementation in RuntimeService.swift (lines 508-540), the stop process follows this sequence:
- State Transition: The container state moves from
.runningto.stopping - Signal Delivery: The runtime sends the specified signal (or
SIGTERMby default) to the container's init process - Timeout Wait: A timer starts for the duration specified in
timeoutInSeconds - Conditional Kill: If the container is still running after the timeout expires, the runtime sends
SIGKILL - Final State: The container transitions to
.stoppedregardless of which signal terminated it
This ensures that containers cannot hang indefinitely while still providing a window for graceful cleanup.
Using the CLI to Stop Containers
The command-line interface provides the most direct way to gracefully stop a container with a custom timeout. The StopCommand.swift implementation parses the --signal and --timeout flags before constructing the ContainerStopOptions and invoking RuntimeClient.stop.
Stop a single container with a 15-second grace period:
container stop my-app --timeout 15
Specify both a custom signal and timeout:
container stop my-app --timeout 20 --signal SIGTERM
If you omit the --timeout flag, the system defaults to 10 seconds. If you omit the --signal flag, it defaults to SIGTERM.
Programmatic Implementation with Swift
You can also trigger graceful stops programmatically using the RuntimeClient API. This approach is useful when building container management tools or integrating with orchestration systems.
RuntimeClient Approach
The RuntimeClient.swift file (lines 181-192) provides a wrapper that packages the ContainerStopOptions into an XPC message and forwards it to the runtime service.
import ContainerResource
import RuntimeClient
let client = RuntimeClient(containerID: "my-app")
let stopOpts = ContainerStopOptions(signal: "SIGTERM", timeoutInSeconds: 15)
Task {
do {
try await client.stop(options: stopOpts)
print("Container stopped gracefully")
} catch {
print("Failed to stop container: \(error)")
}
}
RuntimeService Internals
For developers extending the runtime, the core stop logic in RuntimeService.swift demonstrates how the timeout and signal interact:
public func stop(_ message: XPCMessage) async throws -> XPCMessage {
let stopOptions = try message.stopOptions()
let signal = try Signal(stopOptions.signal ?? "SIGTERM")
let timeout: Duration = .seconds(stopOptions.timeoutInSeconds)
// Move to the "stopping" state first
await self.setState(.stopping)
// Send the signal to the container's init process
try await self.sendSignal(to: self.containerPID, signal: signal)
// Wait for the container to exit or for the timeout to elapse
try await withTimeout(timeout) {
try await self.waitForContainerExit()
}
// If the container is still running after the timeout, force-kill it
if await self.state == .stopping {
try await self.sendSignal(to: self.containerPID, signal: .kill)
}
// Final state transition
await self.setState(.stopped)
return XPCMessage() // empty success response
}
This implementation guarantees that the container reaches the stopped state even if it ignores the initial termination signal.
Default Values and Edge Cases
Understanding the default behavior and edge cases helps prevent unexpected container behavior in production environments.
Default Timeout Behavior If you do not specify a timeout value, the system uses a 10-second default grace period. This provides a reasonable balance between allowing cleanup time and preventing indefinite hangs.
Idempotent Operations The stop operation is idempotent—attempting to stop an already-stopped container returns success without error. This makes the API safe to call in retry loops or concurrent scenarios.
Race Conditions
As demonstrated in TestCLIRmRace.swift, attempting to remove a container while it is still in the stopping state can result in errors like "container is not yet stopped and cannot be deleted." Always ensure the container reaches the .stopped state before removal, or handle the corresponding error appropriately.
Summary
- Primary Keyword Implementation: Use
ContainerStopOptionswithtimeoutInSecondsto control how long the runtime waits before force-killing a container. - Default Configuration: The system defaults to 10 seconds and SIGTERM when you gracefully stop a container without specifying custom parameters.
- Core Files:
ContainerStopOptions.swiftdefines the options structure,RuntimeService.swift(lines 508-540) implements the timeout logic, andRuntimeClient.swift(lines 181-192) provides the client-side wrapper. - Safety Mechanisms: The runtime automatically escalates to SIGKILL after the timeout expires, ensuring containers cannot ignore the shutdown request indefinitely.
- CLI Usage: Use
container stop <id> --timeout <seconds>for command-line container management.
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 provide a timeoutInSeconds value in ContainerStopOptions, the runtime waits 10 seconds after sending SIGTERM before force-killing the container with SIGKILL.
Can I use a custom signal instead of SIGTERM?
Yes. The ContainerStopOptions struct accepts an optional signal parameter as a String. You can specify any valid Unix signal name (such as SIGINT or SIGHUP) either via the CLI --signal flag or programmatically when constructing the options struct. If omitted, it defaults to SIGTERM.
Is the stop operation idempotent?
Yes. The stop operation is idempotent, meaning you can safely call it multiple times on the same container. If the container is already in the .stopped state, the operation returns success without performing any additional actions or raising errors.
What happens if the container ignores the termination signal?
If the container process does not exit within the specified timeout period, the runtime in RuntimeService.swift automatically sends SIGKILL (signal 9) to force immediate termination. This guarantees that the container eventually reaches the .stopped state, regardless of whether the application handles the initial signal gracefully.
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 →