# How to Use Init Images to Customize Container Boot Behavior in apple/container

> Customize apple/container boot behavior using init images. Learn to replace default VM init filesystems with custom images for arbitrary code execution before vminitd.

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

---

**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`](https://github.com/apple/container/blob/main/Sources/Services/ContainerAPIService/Client/Flags.swift) through the `initImage` property. This value propagates through [`Sources/ContainerCommands/Container/ContainerRun.swift`](https://github.com/apple/container/blob/main/Sources/ContainerCommands/Container/ContainerRun.swift) and reaches the server-side implementation in [`Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift`](https://github.com/apple/container/blob/main/Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift).

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

```go
// 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:

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

```dockerfile

# 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:

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

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

```bash
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-image` flag overrides the default init filesystem defined in the `[vminit]` runtime configuration section.
- Custom init images must contain a wrapper binary that eventually `exec`s the real `vminitd` (conventionally located at `/sbin/vminitd.real`).
- Flag processing occurs in [`Flags.swift`](https://github.com/apple/container/blob/main/Flags.swift), [`ContainerRun.swift`](https://github.com/apple/container/blob/main/ContainerRun.swift), and [`ContainersService.swift`](https://github.com/apple/container/blob/main/ContainersService.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.