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/vminitdthat the VM executes as PID-1. - Exec the real init: Your wrapper binary should eventually execute the real
vminitdbinary (typically renamed tovminitd.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/vminitdbinary that eventually execs/sbin/vminitd.realto preserve the standard init lifecycle. - Build a wrapper binary for
arm64(or your target architecture), layer it over the officialghcr.io/apple/containerization/vminitimage, and tag it locally. - Launch containers with
container run --init-image <image>and verify execution viacontainer 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →