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-imageflag overrides the default init filesystem defined in the[vminit]runtime configuration section. - Custom init images must contain a wrapper binary that eventually
execs the realvminitd(conventionally located at/sbin/vminitd.real). - Flag processing occurs in
Flags.swift,ContainerRun.swift, andContainersService.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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →