How to Use the Init Process Flag and Custom Init Images in Container

Use the --init flag to run the built-in vminitd init process as PID 1 for signal forwarding and zombie reaping, or specify --init-image <image> to provide a custom init image that replaces the default binary for specialized boot-time logic.

The Apple Container project provides mechanisms to handle PID 1 responsibilities in lightweight VMs. When containers run without proper init handling, user applications may ignore termination signals or leave zombie processes behind. The --init and --init-image flags solve this by injecting proper init management into the container lifecycle according to the source code in apple/container.

Understanding the Init Process in Containers

Why PID 1 Matters

When a container starts, the specified command becomes PID 1 inside the VM. This process is responsible for reaping orphaned child processes and forwarding termination signals like SIGTERM and SIGINT to the running workload. Most user-space applications are not designed to handle these responsibilities, leading to resource leaks and unresponsive containers.

The Built-in Init Solution

The Container CLI provides a lightweight built-in init process called vminitd. When enabled, this binary runs as PID 1, starts the container's main process as a child, and handles signal forwarding and process reaping via waitpid() loops.

Using the --init Flag for Signal Management

The --init flag is defined in Sources/Services/ContainerAPIService/Client/Flags.swift at lines 61-63:

@Flag(name: .customLong("init"),
      help: "Run an init process inside the container that forwards signals and reaps processes")
public var useInit = false

When you pass this flag, the runtime creates a VM where vminitd runs as the first process. This ensures that signals from the host are properly forwarded to your application and that orphaned child processes are cleaned up automatically.

Run a container with the built-in init process:

container run --init ubuntu:latest my-app

The command my-app runs as a child of the lightweight init, which will reap any orphaned processes and forward signals appropriately.

You can also use the flag with container create for later management:

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

Creating and Using Custom Init Images

The --init-image flag allows you to replace the default vminitd binary with a custom image containing specialized boot-time logic. This is declared in Sources/Services/ContainerAPIService/Client/Flags.swift at lines 64-68:

@Option(name: .long,
        help: .init("Use a custom init image instead of the default", valueName: "image"))
public var initImage: String?

Custom init images run inside the same lightweight VM before the OCI container starts, enabling you to inject helper daemons, install eBPF filters, or print diagnostic messages.

Building a Custom Init Wrapper

Below is a minimal Go wrapper that logs a diagnostic message to the kernel log and then hands off to the real vminitd binary (typically renamed vminitd.real):

// wrapper.go
package main

import (
    "os"
    "syscall"
)

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

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

Build the wrapper for the VM's architecture:

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

Create a Dockerfile that replaces the default vminitd with your wrapper:


# Use the same vminit base as the upstream project

FROM ghcr.io/apple/containerization/vminit:0.33.3 AS base

FROM ghcr.io/apple/containerization/vminit:0.33.3
COPY --from=base /sbin/vminitd /sbin/vminitd.real   # keep the original as .real

COPY wrapper /sbin/vminitd                         # replace with our wrapper

Build the custom init image:

container build -t local/custom-init:latest .

Running with --init-image

Execute a container using your custom init image:

container run --name my-container \
    --init-image local/custom-init:latest \
    alpine:latest echo "hello world"

Verifying Custom Init Execution

To confirm that your custom init image ran successfully, check the boot logs:

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

Expected output:

[    0.129230] custom-init: === CUSTOM INIT IMAGE RUNNING ===

This verification method uses the container logs --boot command to access kernel-level messages from the VM initialization phase.

Summary

  • The --init flag runs the built-in vminitd binary as PID 1 to handle signal forwarding and zombie process reaping automatically.
  • The --init-image <image> flag allows you to specify a custom init image that replaces the default vminitd binary for specialized boot-time behavior.
  • Custom init images typically wrap the original vminitd binary (as vminitd.real) to add diagnostic logging, helper daemons, or other pre-container initialization.
  • Both flags are defined in Sources/Services/ContainerAPIService/Client/Flags.swift and documented in docs/command-reference.md and docs/how-to.md.
  • Verify custom init execution using container logs --boot to check kernel messages from the VM initialization phase.

Frequently Asked Questions

What is the difference between --init and --init-image?

The --init flag enables the default built-in init process (vminitd) that handles signal forwarding and zombie reaping. The --init-image flag specifies a custom OCI image to use instead of the default init binary, allowing you to inject custom boot-time logic while still maintaining init responsibilities. When using --init-image, you typically still need the underlying init functionality, so custom images often wrap and then exec the original vminitd binary.

How do I build a custom init image for the Container CLI?

Build a binary that performs your custom initialization (such as logging to /dev/kmsg or setting up eBPF filters), then execs /sbin/vminitd.real to hand off to the standard init process. Package this binary in a Docker image based on ghcr.io/apple/containerization/vminit, replacing /sbin/vminitd with your wrapper while keeping the original as vminitd.real. Build for the target architecture (typically arm64 or amd64) with CGO_ENABLED=0.

Can I use custom init images with container create?

Yes, the --init-image flag works with both container run and container create commands. When used with container create, the custom init image is configured in the container metadata and used when the container subsequently starts via container start.

Where are the init flags defined in the source code?

The --init and --init-image flags are declared in Sources/Services/ContainerAPIService/Client/Flags.swift at lines 61-68. The --init flag uses the @Flag property wrapper with a custom long name, while --init-image uses the @Option property wrapper. User-level documentation appears in docs/how-to.md (lines 41-55) and docs/command-reference.md (lines 56-58).

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 →