How to Implement Custom Init Images for Advanced Container Startup Logic in Apple Container

Supply a custom OCI image containing a /sbin/vminitd wrapper binary using the --init-image flag to execute arbitrary code before the real init process starts in the Apple Container VM.

The Apple Container CLI (container) runs each workload inside a lightweight virtual machine that boots with a default vminitd image. When you need advanced startup behavior—such as injecting debugging code, starting auxiliary daemons, or applying eBPF filters before the OCI container starts—you can implement custom init images that replace the default init filesystem while preserving standard PID-1 responsibilities.

Understanding Custom Init Images

By default, the VM boots with ghcr.io/apple/containerization/vminit:<tag>, which provides a minimal init process that forwards signals and re-applies standard PID-1 responsibilities. According to the docs/how-to.md file in the apple/container repository, a custom init image is a regular OCI image that supersedes the default vminitd filesystem, allowing you to run arbitrary code before the real init process takes over.

Key Requirements

A valid custom init image must satisfy two critical constraints:

  • Contain /sbin/vminitd: Your image must provide a binary at /sbin/vminitd that the VM executes as PID-1.
  • Exec the real init: Your wrapper binary should eventually execute the real vminitd binary (typically renamed to vminitd.real) to preserve the normal container lifecycle.

The version tag for the base image is defined in Package.swift as scVersion, which determines the default vminit tag used by the CLI.

Creating a Custom Init Image

Implementing a custom init image requires building a wrapper binary, constructing a Dockerfile that layers your wrapper over the official base image, and tagging the result for local use.

Step 1: Write the Wrapper Binary

Create a wrapper that performs custom startup logic, then hands control to the real init binary. The following Go example from docs/how-to.md writes a marker to the kernel log before executing the original vminitd:

// wrapper.go
package main

import (
    "os"
    "syscall"
)

func main() {
    // Log a custom message 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()
    }

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

This wrapper captures the original arguments and environment, allowing seamless delegation to vminitd.real after your custom logic executes.

Step 2: Build for the Target Architecture

The container VM runs on arm64 (Apple Silicon) by default. Build the wrapper with cross-compilation settings matching your target architecture:

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

If you are targeting a different architecture, adjust GOARCH accordingly before building.

Step 3: Construct the Init Image Dockerfile

Layer your wrapper over the official vminit image. This Dockerfile pattern preserves the original binary by copying it to /sbin/vminitd.real, then replaces /sbin/vminitd with your wrapper:


# Use the same vminit tag as defined in Package.swift

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

# Replace it with our wrapper

COPY wrapper /sbin/vminitd

This approach ensures the VM can still access the original init logic after your wrapper completes its execution.

Step 4: Build and Tag the Custom Image

Use the container build command to create the image locally:

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

The resulting image contains your wrapper at /sbin/vminitd and the preserved original at /sbin/vminitd.real.

Running Containers with Your Custom Init Image

Once built, reference your custom init image when launching containers to intercept the VM boot process.

Using the --init-image Flag

Pass the --init-image flag to container run to specify your custom init image:

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

The CLI loads the specified init image into the VM before launching the OCI container. The VM boots your custom image, executes your wrapper as PID-1, and then hands control to the real init process via syscall.Exec.

Verifying Custom Init Execution

Confirm your wrapper executed by inspecting the VM boot logs. The example wrapper writes to /dev/kmsg, which appears in the boot log stream:

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

Expected output:

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

This verification confirms your custom logic ran before the main container process started.

Advanced Use Cases and Considerations

Custom init images provide a hook that runs once per VM boot, making them ideal for several advanced scenarios:

  • Debugging instrumentation: Inject diagnostic tools or tracing agents before the workload starts.
  • Security policies: Apply eBPF filters or seccomp profiles early in the boot process.
  • Auxiliary services: Launch sidecar daemons that must be available before the main container initializes.

As implemented in apple/container, the scripts/install-init.sh utility can build a local copy of the base vminit image when you need to inspect the default behavior or modify the base layer. Additionally, the docs/container-system-config.md file details how the vminit configuration block determines which init image the system uses by default.

Summary

  • Custom init images in Apple Container allow arbitrary code execution during the VM boot phase before the OCI container starts.
  • Your custom image must provide a /sbin/vminitd binary that eventually execs /sbin/vminitd.real to preserve the standard init lifecycle.
  • Build a wrapper binary for arm64 (or your target architecture), layer it over the official ghcr.io/apple/containerization/vminit image, and tag it locally.
  • Launch containers with container run --init-image <image> and verify execution via container logs --boot.

Frequently Asked Questions

What happens if my custom init image does not contain /sbin/vminitd?

The VM will fail to boot because the container runtime expects to execute /sbin/vminitd as the init process. Always ensure your image provides this binary, even if it is a wrapper that eventually chains to another executable.

Can I use a custom init image with any OCI container image?

Yes. The --init-image flag is independent of the container image you specify. You can use a custom init image with any OCI-compliant image (such as alpine:latest or ubuntu:latest) as the workload.

How do I determine which vminit tag to use in my Dockerfile?

Check the scVersion constant in Package.swift at the root of the apple/container repository. This variable defines the default tag used by the CLI, ensuring compatibility between your custom init and the runtime expectations.

Is the custom init image executed for every container restart?

The custom init runs once per VM boot, not per container restart. Since the Apple Container architecture runs containers inside lightweight VMs, the init process only executes when the VM first boots. If the container stops but the VM remains running, the init does not re-execute.

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 →