# How to Gracefully Stop a Container with a Custom Timeout in apple/container

> Learn how to gracefully stop an apple/container with a custom timeout. Send SIGTERM, wait, and automatically force-kill if needed using the --timeout flag or ContainerStopOptions.

- Repository: [Apple/container](https://github.com/apple/container)
- Tags: how-to-guide
- Published: 2026-06-18

---

**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`](https://github.com/apple/container/blob/main/ContainerStopOptions.swift) encapsulates the two configurable parameters for container shutdown:

- **signal**: An optional `String` representing the Unix signal to send (defaults to `SIGTERM` if not specified)
- **timeoutInSeconds**: An `Int` representing the grace period before forceful termination (defaults to 10 seconds)

Here is how the options are structured in the source:

```swift
import ContainerResource

let stopOpts = ContainerStopOptions(signal: "SIGTERM", timeoutInSeconds: 15)

```

### State Transitions and Signal Handling

According to the implementation in [`RuntimeService.swift`](https://github.com/apple/container/blob/main/RuntimeService.swift) (lines 508-540), the stop process follows this sequence:

1. **State Transition**: The container state moves from `.running` to `.stopping`
2. **Signal Delivery**: The runtime sends the specified signal (or `SIGTERM` by default) to the container's init process
3. **Timeout Wait**: A timer starts for the duration specified in `timeoutInSeconds`
4. **Conditional Kill**: If the container is still running after the timeout expires, the runtime sends `SIGKILL`
5. **Final State**: The container transitions to `.stopped` regardless 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`](https://github.com/apple/container/blob/main/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:

```bash
container stop my-app --timeout 15

```

Specify both a custom signal and timeout:

```bash
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`](https://github.com/apple/container/blob/main/RuntimeClient.swift) file (lines 181-192) provides a wrapper that packages the `ContainerStopOptions` into an XPC message and forwards it to the runtime service.

```swift
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`](https://github.com/apple/container/blob/main/RuntimeService.swift) demonstrates how the timeout and signal interact:

```swift
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`](https://github.com/apple/container/blob/main/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 `ContainerStopOptions` with `timeoutInSeconds` to 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.swift`](https://github.com/apple/container/blob/main/ContainerStopOptions.swift) defines the options structure, [`RuntimeService.swift`](https://github.com/apple/container/blob/main/RuntimeService.swift) (lines 508-540) implements the timeout logic, and [`RuntimeClient.swift`](https://github.com/apple/container/blob/main/RuntimeClient.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`](https://github.com/apple/container/blob/main/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.