How to Use Init Images to Customize Container Boot Behavior in apple/container

Use the --init-image <image> CLI flag to replace the default VM init filesystem with a custom image that runs arbitrary code before transferring control to the real vminitd binary.

The apple/container project isolates each container inside a lightweight virtual machine. During boot, the VM mounts an init filesystem image that provides the vminitd binary as PID 1. By supplying a custom init image via the --init-image flag, you can instrument the earliest stages of the VM lifecycle—before the OCI container image ever starts—to perform logging, load kernel modules, or configure eBPF filters.

How Init Images Work in the VM Architecture

Every container runs within a VM that boots from a minimal init filesystem. By default, this filesystem is defined in the runtime configuration under the [vminit] section and points to ghcr.io/apple/containerization/vminit:0.34.0. This image contains the vminitd binary, which acts as the container’s init process and manages the transition into the user-provided OCI container.

A custom init image is a small Linux filesystem that replaces the original binary with a wrapper. This wrapper executes your custom boot-time logic and then transfers control to the real vminitd via exec, ensuring the container runtime initializes correctly.

The --init-image Flag Implementation

The --init-image flag is parsed in Sources/Services/ContainerAPIService/Client/Flags.swift through the initImage property. This value propagates through Sources/ContainerCommands/Container/ContainerRun.swift and reaches the server-side implementation in Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift.

In ContainersService.swift, the runtime fetches the specified image and mounts it as the init filesystem before the VM begins booting. The default unpack strategy is provided by Sources/Services/ContainerAPIService/Server/SnapshotStore.swift, ensuring the image is available before vminitd is invoked.

Building a Custom Init Image

Creating a custom init image requires building a wrapper binary that will run as PID 1 inside the VM.

Step 1: Write a Wrapper Binary

The wrapper must perform your custom initialization and then exec the original vminitd. Below is a minimal Go implementation that logs a message to the kernel buffer before handing off control:

// wrapper.go – minimal Go wrapper for vminitd
package main

import (
    "os"
    "syscall"
)

func main() {
    // Write a custom message to the 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 over to the real vminitd binary
    err = syscall.Exec("/sbin/vminitd.real", os.Args, os.Environ())
    if err != nil {
        os.Exit(1)
    }
}

Step 2: Cross-Compile for the Target Architecture

VMs in apple/container typically use the arm64 architecture. Build your wrapper with static linking to ensure it runs in the minimal init environment:

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

Step 3: Create a Containerfile

Use the official vminit base image to ensure kernel compatibility. Rename the original binary to vminitd.real and install your wrapper as /sbin/vminitd:


# Containerfile for custom init image

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

FROM ghcr.io/apple/containerization/vminit:0.34.0

# Preserve the original binary

COPY --from=base /sbin/vminitd /sbin/vminitd.real

# Install the wrapper as the new init

COPY wrapper /sbin/vminitd

Step 4: Build the Custom Image

Use the container CLI to build your init image:

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

Running and Verifying the Custom Init

Pass your custom image to the --init-image flag when creating or running a container:

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

Verify that your wrapper executed by inspecting the VM boot logs:

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

You should see output similar to:


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

Summary

  • The --init-image flag overrides the default init filesystem defined in the [vminit] runtime configuration section.
  • Custom init images must contain a wrapper binary that eventually execs the real vminitd (conventionally located at /sbin/vminitd.real).
  • Flag processing occurs in Flags.swift, ContainerRun.swift, and ContainersService.swift, with image mounting handled before VM boot.
  • Init images run as PID 1 inside the VM, allowing kernel-level instrumentation before the OCI container starts.
  • Verify execution by grepping the output of container logs --boot <container-name>.

Frequently Asked Questions

What is the default init image used by apple/container?

The default image is specified in the runtime configuration under the [vminit] section and typically resolves to ghcr.io/apple/containerization/vminit:0.34.0. This image provides the vminitd binary that serves as the container’s init process.

What architecture should I build my init wrapper for?

You should cross-compile your wrapper for linux/arm64, as this is the architecture used by the lightweight VMs in apple/container. Use CGO_ENABLED=0 to produce a statically linked binary that does not depend on external libraries in the minimal init environment.

How does the wrapper transfer control back to the real init system?

After performing custom boot-time work, the wrapper should call syscall.Exec() (or equivalent) to replace its own process with the original vminitd binary. Conventionally, the original binary is preserved as /sbin/vminitd.real in the custom init image, and the wrapper is installed at /sbin/vminitd.

Can I use init images to modify container runtime policies?

Init images execute inside the VM before the OCI container starts, making them suitable for VM-level customization such as loading kernel modules, configuring networking, or setting up eBPF filters. They cannot directly modify container runtime policies like seccomp profiles or capabilities, which are enforced by the container runtime after the init phase completes.

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 →