How to Create a Custom Init Image for VM Boot Customization in Container
You can create a custom init image by building a wrapper binary that executes your logic before calling the real vminitd, packaging it into a Docker image layered on the official vminit base, and running containers with the --init-image flag pointing to your custom image.
Apple Container's lightweight virtualization stack uses the vminitd binary as the VM's init process. By creating a custom init image, you can inject arbitrary code—such as debugging instrumentation, eBPF filters, or kernel-level services—before the standard boot sequence takes over. This workflow leverages the official vminit image as a base and requires only a simple wrapper binary and a Dockerfile, as documented in the apple/container repository.
Write a Custom Wrapper for vminitd
The vminitd binary serves as the default init process for Container VMs. To customize the boot sequence, you must create a wrapper that executes your custom logic and then invokes the original vminitd binary.
Create the Wrapper Binary
According to docs/how-to.md (lines 583–588), the wrapper should perform your custom initialization and then replace the current process with the real vminitd using syscall.Exec. The following Go example writes a marker to the kernel log before handing control to the standard init:
package main
import (
"log"
"os"
"syscall"
)
func main() {
// Custom behavior: write a marker to kernel log for verification
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()
} else {
log.Printf("cannot open /dev/kmsg: %v", err)
}
// Replace current process with the real vminitd
err = syscall.Exec("/sbin/vminitd.real", os.Args, os.Environ())
if err != nil {
log.Fatalf("exec failed: %v", err)
}
}
Compile the Wrapper
Compile the wrapper binary locally. This binary will be copied into your Docker image in the next step.
go build -o wrapper ./wrapper/main.go
Build the Custom Init Image
As shown in docs/how-to.md (lines 608–615), construct a Dockerfile that layers your wrapper on top of the official vminit image. The Dockerfile preserves the original vminitd binary by renaming it to vminitd.real and installs your wrapper as the new /sbin/vminitd entry point.
FROM ghcr.io/apple/containerization/vminit:0.33.3 AS base
FROM ghcr.io/apple/containerization/vminit:0.33.3
# Preserve the original vminitd binary
COPY --from=base /sbin/vminitd /sbin/vminitd.real
# Install the wrapper as the new init binary
COPY wrapper /sbin/vminitd
Build and tag the image using the Container CLI:
container build -t local/custom-init:latest .
Launch Containers with Your Custom Init Image
To boot a VM with your custom initialization logic, pass the --init-image flag to container run, specifying the tag of the image you built. This flag instructs Container to use your custom image instead of the default vminit image.
As documented in docs/how-to.md (line 624), the syntax is:
container run \
--name my-container \
--init-image local/custom-init:latest \
alpine:latest \
echo "hello"
The VM will execute your wrapper first, which performs any custom initialization tasks before calling the real vminitd to continue the standard boot process.
Verify the Custom Init Ran
Confirm that your custom init image executed by inspecting the VM boot logs. Use the container logs --boot command to retrieve early boot output and grep for the marker string your wrapper wrote to /dev/kmsg:
container logs --boot my-container | grep custom-init
If the custom init ran successfully, you will see output similar to:
[ 0.129230] custom-init: === CUSTOM INIT IMAGE RUNNING ===
Install the Default Init Image from Source
When developing the Container SDK locally, you may need to rebuild and load the default init image rather than using a custom one. The scripts/install-init.sh script automates this process by building the vminit image from the local source and loading it into the container runtime.
The script performs the following operations:
IMAGE_NAME="vminit:latest"
make -C ${CONTAINERIZATION_PATH} init
${CONTAINERIZATION_PATH}/bin/cctl images save -o /tmp/init.tar ${IMAGE_NAME}
bin/container i load -i /tmp/init.tar
rm /tmp/init.tar
Running this script is only necessary when you require an editable version of the default vminit image for development purposes.
Summary
- Custom init images allow you to execute arbitrary code before the standard
vminitdinit process takes over in Container VMs. - The implementation requires a wrapper binary that calls the original
vminitdviasyscall.Execafter performing custom logic. - Build the image using a Dockerfile that layers your wrapper on top of
ghcr.io/apple/containerization/vminitand preserves the original binary asvminitd.real. - Launch containers with the
--init-imageflag to specify your custom image. - Verify execution by checking boot logs with
container logs --bootand searching for your custom kernel log markers. - Use
scripts/install-init.shto rebuild the default init image when developing the Container SDK locally.
Frequently Asked Questions
What is the purpose of a custom init image in Container?
A custom init image allows you to customize the VM boot sequence by running arbitrary code—such as debugging tools, eBPF filters, or additional daemons—before the standard vminitd init process takes over. This is useful for instrumenting the boot sequence, adding VM-level services, or modifying kernel parameters that must be set early in the boot process.
How does the wrapper binary interact with the original vminitd?
The wrapper binary executes your custom logic and then invokes the original vminitd binary using syscall.Exec to replace the current process. In the Dockerfile, the original /sbin/vminitd is renamed to /sbin/vminitd.real, allowing the wrapper installed at /sbin/vminitd to call the real init process after completing its custom tasks.
Can I use languages other than Go for the custom init wrapper?
Yes. While the example in docs/how-to.md uses Go, you can write the wrapper in any compiled language that supports process replacement or execution. The requirements are that the binary must be statically linked (or compatible with the VM environment) and must ultimately execute /sbin/vminitd.real to allow the normal boot process to continue.
Where is the official vminit image stored?
The official vminit image is hosted at ghcr.io/apple/containerization/vminit with tags corresponding to the Containerization SDK version (e.g., 0.33.3). Your Dockerfile should use this image as both the base and the final stage to ensure compatibility with the Container runtime.
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 →