# How Container Handles Signal Forwarding and Zombie Process Reaping

> Learn how apple/container handles signal forwarding and zombie process reaping with its vminitd init process. Discover efficient process management in containers.

- Repository: [Apple/container](https://github.com/apple/container)
- Tags: internals
- Published: 2026-06-21

---

**The `apple/container` runtime solves signal forwarding and zombie process reaping by launching a lightweight init process called `vminitd` as PID 1 when you use the `--init` flag, which forwards signals to the application and reaps child processes using `waitpid()`.**

When you start a container without special handling, the command specified in `container run` executes as PID 1 inside the VM. In Unix systems, PID 1 carries unique responsibilities: it must forward signals to child processes and reap terminated children to prevent zombie accumulation. Most application code does not implement these kernel-level duties, leading to containers that ignore `SIGTERM` requests or leak zombie processes. The Container project addresses this through an optional init system that runs automatically when enabled via CLI flags.

## The PID 1 Problem in Containerized Workloads

Without an init process, your application becomes the root process of the container's PID namespace. This creates two critical failure modes:

- **Signal handling failures**: The kernel sends signals like `SIGINT` and `SIGTERM` to PID 1, but if the application doesn't forward them to worker processes, graceful shutdown becomes impossible.
- **Zombie process accumulation**: When child processes exit, they become zombies until the parent calls `waitpid()`. If PID 1 never reaps these children, the process table fills with defunct entries, eventually exhausting system resources.

## Signal Forwarding and Zombie Process Reaping with `--init`

Container solves the PID 1 dilemma through its `--init` flag, which injects a minimal init system into the container VM before the application starts.

### The `vminitd` Init Process

When you pass `--init`, the runtime launches `vminitd` as PID 1 instead of your application. This binary, built via [`scripts/install-init.sh`](https://github.com/apple/container/blob/main/scripts/install-init.sh) and packaged with the project, acts as a proxy between the kernel and your actual application process. The init process starts immediately during the VM boot sequence, receiving the init image through `ContainerCreateOptions` before the main container creation completes.

### Signal Forwarding to the Application

`vminitd` receives all signals sent to the container because it occupies PID 1. Upon receiving signals like `SIGINT` or `SIGTERM`, it forwards them to the actual application process, which runs as its direct child. This ensures that termination requests from the host reach the application code, enabling graceful shutdowns even for programs that lack explicit signal handling.

### Automatic Zombie Reaping via `waitpid()`

The init process continuously monitors child process states using `waitpid()`. When any child process terminates—whether the main application or transient workers—`vminitd` immediately reaps the process, preventing zombie accumulation. This automatic reaping happens transparently without requiring modifications to the application code.

## CLI Implementation and Runtime Safety

The Container CLI exposes init functionality through dedicated flags and adds protective mechanisms for interactive sessions.

### The `--init` Flag Definition

The `--init` flag surface appears in the Swift source code as a custom long flag:

```swift
// Sources/Services/ContainerAPIService/Client/Flags.swift
@Flag(name: .customLong("init"), help: "Run an init process inside the container that forwards signals and reaps processes")

```

When present, this flag triggers the runtime to inject the default `vminitd` image into the container VM. For custom init behavior, you can alternatively use `--init-image` to specify your own init binary.

### Signal Threshold Protection for Interactive Sessions

During interactive runs (without `--detach`), the CLI installs a safeguard against runaway signal loops. The `SignalThreshold` helper aborts the container if it receives too many termination signals:

```swift
// Sources/ContainerCommands/Container/ContainerRun.swift
if !self.processFlags.tty {
    var handler = SignalThreshold(threshold: 3, signals: [SIGINT, SIGTERM])
    let log = self.log
    handler.start {
        log.warning("Received 3 SIGINT/SIGTERM's, forcefully exiting.")
        Darwin.exit(1)
    }
}

```

This protection triggers after three consecutive `SIGINT` or `SIGTERM` signals, preventing the host from hanging due to unresponsive containers while still allowing the init process to handle normal signal forwarding.

## Practical Usage Examples

Run a container with automatic signal forwarding and zombie reaping:

```bash
container run --init ubuntu:latest my-app

```

Create a container with init capabilities without starting it:

```bash
container create --init --name my-container ubuntu:latest my-app

```

Use a custom init image for specialized debugging or extended functionality:

```bash
container run --init-image local/custom-init:latest alpine:latest echo "hello"

```

## Summary

- **PID 1 responsibilities**: Without an init system, the container's main process must handle signal forwarding and child reaping, which most applications omit.
- **`vminitd` as PID 1**: The `--init` flag launches `vminitd` to manage these kernel duties, forwarding signals to the application and reaping zombies via `waitpid()`.
- **CLI integration**: The flag is defined in [`Sources/Services/ContainerAPIService/Client/Flags.swift`](https://github.com/apple/container/blob/main/Sources/Services/ContainerAPIService/Client/Flags.swift) and processed during `ContainerCreateOptions` propagation.
- **Safety mechanisms**: Interactive sessions include a `SignalThreshold` watchdog in [`Sources/ContainerCommands/Container/ContainerRun.swift`](https://github.com/apple/container/blob/main/Sources/ContainerCommands/Container/ContainerRun.swift) that forcefully exits after three `SIGINT`/`SIGTERM` signals.
- **Customization**: Use `--init-image` to replace the default `vminitd` binary built by [`scripts/install-init.sh`](https://github.com/apple/container/blob/main/scripts/install-init.sh) with your own init implementation.

## Frequently Asked Questions

### What happens if I run a container without the `--init` flag?

Your application runs as PID 1 and must explicitly implement signal handling and child process reaping. If it does not, the container will ignore termination signals like `SIGTERM` and accumulate zombie processes when child processes exit, potentially leading to resource exhaustion and unresponsive containers.

### How does `vminitd` forward signals to the actual application?

`vminitd` runs as PID 1, making it the target for all kernel signals sent to the container. It captures these signals and forwards them to the application process, which runs as a direct child of the init system. This proxy mechanism ensures that signal-based lifecycle management from the host reaches the application code.

### Can I use a custom init process instead of the default `vminitd`?

Yes. Instead of `--init`, use the `--init-image` flag to specify a custom container image containing your init binary. The runtime launches this image as PID 1, allowing you to implement custom signal handling logic, add debugging hooks, or include additional initialization steps before the main application starts.

### When does the signal threshold watchdog activate?

The watchdog activates during interactive runs (when `--detach` is not used) and monitors for three consecutive `SIGINT` or `SIGTERM` signals. Upon reaching this threshold, defined in [`Sources/ContainerCommands/Container/ContainerRun.swift`](https://github.com/apple/container/blob/main/Sources/ContainerCommands/Container/ContainerRun.swift), the CLI forcefully exits with code 1 to prevent the host terminal from hanging due to an unresponsive container process.