# How to Create a Custom Init Image for VM Boot Customization in Container

> Learn how to create a custom init image for VM boot customization in container. Build a wrapper binary, package it, and use the --init-image flag for advanced control.

- Repository: [Apple/container](https://github.com/apple/container)
- Tags: how-to-guide
- Published: 2026-06-20

---

**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`](https://github.com/apple/container/blob/main/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:

```go
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.

```bash
go build -o wrapper ./wrapper/main.go

```

## Build the Custom Init Image

As shown in [`docs/how-to.md`](https://github.com/apple/container/blob/main/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.

```dockerfile
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:

```bash
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`](https://github.com/apple/container/blob/main/docs/how-to.md) (line 624), the syntax is:

```bash
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`:

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

```

If the custom init ran successfully, you will see output similar to:

```text
[    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`](https://github.com/apple/container/blob/main/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:

```bash
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 `vminitd` init process takes over in Container VMs.
- The implementation requires a **wrapper binary** that calls the original `vminitd` via `syscall.Exec` after performing custom logic.
- Build the image using a Dockerfile that layers your wrapper on top of `ghcr.io/apple/containerization/vminit` and preserves the original binary as `vminitd.real`.
- Launch containers with the `--init-image` flag to specify your custom image.
- Verify execution by checking boot logs with `container logs --boot` and searching for your custom kernel log markers.
- Use [`scripts/install-init.sh`](https://github.com/apple/container/blob/main/scripts/install-init.sh) to 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`](https://github.com/apple/container/blob/main/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.