# How to Use the Init Process to Handle Zombie Processes in Apple Container

> Learn how to manage zombie processes in Apple containers using the --init flag. This injects vminitd as PID 1 to auto-reap zombies and forward signals effectively.

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

---

**Use the `--init` flag when running containers to inject the `vminitd` init process as PID 1, which automatically reaps zombie processes via `waitpid()` and forwards signals to your application.**

Zombie processes accumulate in containers when the main process lacks init system responsibilities. The `apple/container` repository solves this with a lightweight init process that runs as PID 1 to reap orphaned children and handle signal forwarding, preventing the PID exhaustion that un-reaped zombies cause.

## Why Zombie Processes Occur Without an Init System

When you start a container without the `--init` flag, your specified command becomes **PID 1** inside the lightweight virtual machine. In Linux, PID 1 has two critical responsibilities that most applications do not implement:

1. **Signal forwarding** – receiving signals like `SIGTERM` and `SIGINT` from the kernel and propagating them to child processes.
2. **Reaping orphaned children** – calling `waitpid()` to collect exit statuses when child processes terminate.

Without these behaviors, terminated child processes enter a **zombie state** (defunct processes showing as `<defunct>` in `ps` output). The kernel keeps these entries in the process table because PID 1 never collected their exit status. Over time, zombies waste PID slots and can eventually prevent new process creation.

## Enabling the Init Process with the --init Flag

The Container toolchain provides the `vminitd` binary as a lightweight init process. When you pass `--init` to `container run` or `container create`, the tooling inserts `vminitd` as PID 1 and runs your command as a child process.

The default init image is `ghcr.io/apple/containerization/vminit:<tag>`, which contains the `vminitd` binary. As documented in [`docs/command-reference.md`](https://github.com/apple/container/blob/main/docs/command-reference.md) (lines 56-57) and the "Run a container with a provided init process" section of [`docs/how-to.md`](https://github.com/apple/container/blob/main/docs/how-to.md) (lines 43-44), this flag eliminates zombie accumulation without requiring changes to your application code.

```bash

# Run with the built-in init process

container run --init ubuntu:latest my-application

# Or create then start

container create --init --name my-container ubuntu:latest my-application
container start my-container

```

### Signal Forwarding and Zombie Reaping

The `vminitd` process handles the two PID 1 responsibilities that prevent zombie accumulation:

- **Continuous reaping**: It calls `waitpid(-1, ...)` in a loop to collect exit statuses from any orphaned child process, immediately removing zombies from the process table.
- **Signal propagation**: It receives signals from the kernel and forwards them to the real application process, ensuring graceful shutdowns work correctly.

This behavior is implemented in the `vminitd` binary located at `/sbin/vminitd` within the init image.

## Verifying Zombie Process Handling

The integration test `TestCLIRunCommand` in [`Tests/IntegrationTests/Run/TestCLIRunCommand.swift`](https://github.com/apple/container/blob/main/Tests/IntegrationTests/Run/TestCLIRunCommand.swift) (lines 553-564) validates that containers run with `--init` leave zero zombie processes. The test spawns short-lived child processes and asserts that `zombieCount == 0` after they exit.

You can verify this behavior manually by spawning background processes that exit immediately:

```bash

# Start a container that spawns a short-lived child

container run --init --rm ubuntu:latest sh -c "sleep 1 & wait"

# Inspect from the host to confirm no zombies remain

# The test suite automates this verification

```

Without `--init`, the same command would leave zombie entries because the shell running as PID 1 would not reap the background `sleep` process.

## Building a Custom Init Image

For scenarios requiring additional initialization—such as configuring network settings or writing to `/dev/kmsg` before the main process starts—you can supply a **custom init image** via the `--init-image` flag. The custom image wraps the default `vminitd` binary, performs additional work, then `exec`s the real init binary.

As shown in [`docs/how-to.md`](https://github.com/apple/container/blob/main/docs/how-to.md) (lines 68-89), the wrapper must eventually execute `/sbin/vminitd.real` to assume init responsibilities.

**Step 1 – Create a Go wrapper:**

```go
// wrapper.go
package main

import (
    "os"
    "syscall"
)

func main() {
    // Optional: Write debugging info to kernel log
    kmsg, err := os.OpenFile("/dev/kmsg", os.O_WRONLY, 0)
    if err == nil {
        kmsg.WriteString("<6>custom-init: === CUSTOM INIT IMAGE RUNNING ===\n")
        kmsg.Close()
    }

    // Hand off to the real vminitd binary
    if err := syscall.Exec("/sbin/vminitd.real", os.Args, os.Environ()); err != nil {
        os.Exit(1)
    }
}

```

**Step 2 – Build the wrapper:**

```bash
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o wrapper wrapper.go

```

**Step 3 – Construct the Containerfile:**

```dockerfile
FROM ghcr.io/apple/containerization/vminit:0.34.0 AS base

FROM ghcr.io/apple/containerization/vminit:0.34.0
COPY --from=base /sbin/vminitd /sbin/vminitd.real
COPY wrapper /sbin/vminitd

```

**Step 4 – Build and run:**

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

```

Confirm the custom logic executed by checking the VM boot log:

```bash
container logs --boot my-container | grep custom-init

# → <6>custom-init: === CUSTOM INIT IMAGE RUNNING ===

```

The [`scripts/install-init.sh`](https://github.com/apple/container/blob/main/scripts/install-init.sh) script provides a helper for building the `vminit` image and loading it into the container VM during development.

## Summary

- **Use `--init`** to inject `vminitd` as PID 1, which automatically reaps zombie processes via `waitpid(-1, ...)` and forwards signals to your application.
- **Without `--init`**, your command runs as PID 1 and likely accumulates zombies because it lacks the required reaping logic.
- **Use `--init-image`** to supply a custom init wrapper when you need pre-startup initialization logic, ensuring the wrapper eventually executes `/sbin/vminitd.real`.
- **Verification** is available through the integration tests in [`Tests/IntegrationTests/Run/TestCLIRunCommand.swift`](https://github.com/apple/container/blob/main/Tests/IntegrationTests/Run/TestCLIRunCommand.swift), which assert zero zombies after container execution.

## Frequently Asked Questions

### What is the default init image used by the `--init` flag?

The default init image is `ghcr.io/apple/containerization/vminit:<tag>`, which contains the `vminitd` binary. This binary runs as PID 1 to handle signal forwarding and continuously calls `waitpid()` to reap orphaned child processes, preventing zombie accumulation.

### How does the init process prevent zombie processes?

The `vminitd` process prevents zombies by calling `waitpid(-1, ...)` in a continuous loop. This system call collects the exit status of any terminated child process, allowing the kernel to remove the process from the process table immediately rather than leaving it in a zombie state.

### Can I run additional setup scripts before my main application starts?

Yes, by using the `--init-image` flag to specify a custom init image. You can create a wrapper script or binary that performs initialization tasks—such as writing to `/dev/kmsg` or configuring the environment—and then uses `exec` to replace itself with the real `vminitd` binary at `/sbin/vminitd.real`.

### Where is the init process functionality tested in the source code?

The zombie reaping behavior is validated in [`Tests/IntegrationTests/Run/TestCLIRunCommand.swift`](https://github.com/apple/container/blob/main/Tests/IntegrationTests/Run/TestCLIRunCommand.swift) (lines 553-564), which runs a container with the `--init` flag and asserts that `zombieCount == 0`. Additional tests for custom init images reside in [`Tests/IntegrationTests/Run/TestCLIRunInitImage.swift`](https://github.com/apple/container/blob/main/Tests/IntegrationTests/Run/TestCLIRunInitImage.swift).