How to Use Custom Init Images for Container Boot-Time Customization in Apple Container

Use the --init-image flag to replace the default vminitd binary with a custom wrapper that executes boot-time logic before handing control to the real init process.

The apple/container project runs each container inside a lightweight VM that boots with an init filesystem image containing the vminitd binary. By leveraging custom init images for container boot-time customization, you can inject arbitrary logic—such as logging, eBPF filter loading, or daemon startup—into the very first code that executes on the VM, before any user-provided container image starts.

What Are Custom Init Images?

Each container runs inside a lightweight VM that boots with an init filesystem image providing the vminitd binary (the container’s init process). By default, this image is defined in the runtime configuration under the [vminit] section, typically pointing to ghcr.io/apple/containerization/vminit:0.34.0.

A custom init image is a small Linux filesystem containing a wrapper binary that replaces the original vminitd. This wrapper performs arbitrary boot-time work and then transfers control to the real vminitd via exec. This approach lets you instrument the VM bootstrap phase before the OCI container workload begins.

How the --init-image Flag Works

The --init-image CLI flag overrides the default init image for a single run or create operation. Internally, the flag propagates through several layers of the Swift codebase:

The default unpack strategy for init images is provided by Sources/Services/ContainerAPIService/Server/SnapshotStore.swift.

Creating a Custom Init Image

Follow this workflow to build and deploy a custom init image that logs a message during VM boot.

Step 1: Write the Wrapper Binary

Create a Go wrapper that writes to the kernel log and then executes the real vminitd. This example uses Go for easy cross-compilation.

// wrapper.go – a 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: Build for the Target Architecture

Build the wrapper statically for the VM architecture (typically arm64).

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

Step 3: Create the Containerfile

Use a multi-stage build that preserves the original vminitd as vminitd.real and installs your wrapper as the new /sbin/vminitd.


# Dockerfile that creates the custom init image

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

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

# Keep the original binary as vminitd.real

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

# Replace it with our wrapper

COPY wrapper /sbin/vminitd

Step 4: Build the Custom Init Image

Build the image using the container CLI.

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

Step 5: Run with the Custom Init Image

Pass the --init-image flag to override the default init image for your container.

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

Step 6: Verify Boot-Time Execution

Inspect the VM boot logs to confirm your wrapper executed.

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

Expected output:


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

Key Implementation Details

The custom init image feature touches several critical files in the apple/container repository:

Summary

  • Custom init images replace the default vminitd binary to inject logic into the VM boot sequence.
  • Use the --init-image <image> flag to specify a custom init image for a single run or create operation.
  • The wrapper binary must exec the real /sbin/vminitd.real after completing custom boot-time work.
  • Build your wrapper for the target VM architecture (usually arm64).
  • Verify execution using container logs --boot <name> to inspect early boot messages.

Frequently Asked Questions

What is the default init image if I don't specify --init-image?

According to the runtime configuration in the [vminit] section, the default image is ghcr.io/apple/containerization/vminit:0.34.0. This image provides the standard vminitd binary that initializes the VM environment before starting the container workload.

Can I use any container image as a custom init image?

No, a valid custom init image must contain a compatible vminitd binary (or wrapper) that can act as the VM's init process. The image should derive from the official vminit base image to ensure compatibility with the VM kernel and versioning, as demonstrated in the Dockerfile example using ghcr.io/apple/containerization/vminit:0.34.0 as both the base and final stage.

How do I debug a custom init image that fails to boot?

Use the container logs --boot <container-name> command to inspect the VM boot logs. This output captures everything from the init process startup, including any output from your wrapper binary before it executes the real vminitd. Look for kernel messages or errors related to your custom init logic.

Is the --init-image flag persistent across container restarts?

No, the --init-image flag applies only to the specific run or create operation where it is specified. It is not stored as part of the container configuration. To use a custom init image persistently, you must specify the flag every time you start or run the container.

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 →