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 --timeout is omitted
  • Default signal: SIGTERM if --signal is 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:

  1. State transition: Sets internal state to .stopping
  2. Signal delivery: Sends the requested signal (e.g., SIGTERM) to the container's PID
  3. Timer activation: Waits for the specified timeoutInSeconds duration
  4. Conditional escalation: If state remains .stopping after timeout, sends SIGKILL
  5. Finalization: Transitions state to .stopped and 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 --signal flag
  • Swift API: Construct ContainerStopOptions and call RuntimeClient.stop(options:)
  • Core implementation: Located in RuntimeService.swift (lines 508-540) with state machine management
  • Safety guarantees: Automatic SIGKILL escalation 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →