Purpose of the `--init` Flag in `container run`: Signal Handling and Zombie Reaping
The --init flag injects a lightweight init process as PID 1 that forwards signals and reaps zombie processes, solving critical issues when your application isn't designed to run as a Linux init system.
When running workloads with the apple/container toolkit, your application typically executes as PID 1 inside the container namespace unless you specify otherwise. The --init flag in container run addresses the architectural challenges that arise when your command must handle responsibilities traditionally reserved for system init processes.
Why PID 1 Matters in Linux Containers
Linux reserves PID 1 for the init system, assigning it unique responsibilities that typical applications don't manage. When you invoke container run without special options, your specified command runs directly as PID 1, forcing it to handle two critical operating system duties:
- Signal forwarding – Receiving and properly dispatching signals (such as
SIGTERMandSIGINT) to child processes in the process tree. - Zombie process reaping – Calling
wait()on terminated child processes to prevent them from becoming defunct "zombie" processes that consume system resources.
Most applications aren't written to act as init systems. They may ignore signals or fail to reap orphans, leading to graceful shutdown failures or resource leaks according to docs/how-to.md in the repository.
How container run --init Works
When you specify --init, the runtime prepends a lightweight init process (similar to Docker’s tini) in front of your command. This init binary becomes PID 1, while your actual application runs as a child process.
Signal Forwarding
The injected init process automatically forwards all received signals to your application. This ensures that when you send stop signals from the host, they reach your application correctly, enabling graceful shutdowns. As implemented in apple/container, this happens entirely within the same user namespace as your container.
Zombie Process Reaping
The init system monitors for orphaned child processes and automatically calls wait() on them. This prevents zombie buildup that would otherwise occur if your application spawned child processes without reaping them. The integration tests in Tests/IntegrationTests/Run/TestCLIRunCommand.swift specifically verify that no zombie processes appear when --init is active.
Practical Usage Examples
The docs/command-reference.md file documents the complete CLI interface for these flags.
Basic Usage with Default Init
For most applications, simply adding --init provides proper signal handling and process management without additional configuration:
# Run with the default init process
container run --init ubuntu:latest my-app
You can also use it with container create for explicit container management:
container create --init --name my-container ubuntu:latest my-app
Custom Init Images with --init-image
When you need to customize the init environment—such as running additional VM-level daemons or applying eBPF filters—pair --init with --init-image:
container run \
--init-image local/custom-init:latest \
alpine:latest \
echo "hello from a container with a custom init"
The default init image is used when --init is specified alone, but --init-image allows providing a custom init filesystem image as detailed in docs/how-to.md.
Implementation and Testing Details
The apple/container repository includes comprehensive integration tests validating init process behavior. The file Tests/IntegrationTests/Run/TestCLIRunCommand.swift contains tests verifying signal forwarding and zombie prevention, while Tests/IntegrationTests/Run/TestCLIRunInitImage.swift handles error cases and edge conditions for custom init images.
Because the init process runs in the same user namespace as your container, it adds virtually no overhead while guaranteeing correct signal propagation and process cleanup.
Summary
- The
--initflag solves the signal handling and zombie reaping problems inherent to running applications as PID 1 incontainer run. - Without
--init, your application must handle Linux init responsibilities itself, which most applications aren't designed to do. - You can customize the init environment using
--init-image <image>alongside--initfor specialized use cases like VM-level daemons. - The implementation is validated by integration tests in
Tests/IntegrationTests/Run/TestCLIRunCommand.swiftthat confirm proper process cleanup.
Frequently Asked Questions
What happens if I run a container without --init?
Your application runs directly as PID 1 and must handle signal forwarding and zombie reaping itself. If your application isn't designed as an init system, it may ignore shutdown signals or accumulate zombie processes, causing resource leaks and preventing graceful termination.
Can I use a custom init binary instead of the default?
Yes. Use the --init-image flag to specify a custom filesystem image containing your init binary. This runs instead of the default init while still providing the PID 1 infrastructure. The Tests/IntegrationTests/Run/TestCLIRunInitImage.swift file tests this functionality and its error handling paths.
Does using --init impact container performance?
No. According to docs/how-to.md, the init process operates within the same user namespace as your container and consumes negligible resources. It adds virtually no overhead while solving critical process management issues.
When should I avoid using --init?
Avoid --init if your application is specifically designed to function as an init system (such as systemd or a custom init) and needs to maintain PID 1 status for its own process management logic. For standard applications, databases, and web servers, --init is recommended to ensure proper signal handling and cleanup.
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 →