# How to Use Custom Init Images for Container Boot-Time Customization in Apple Container

> Customize container boot time with custom init images in Apple Container. Learn to use the --init-image flag to execute custom logic before the init process.

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

---

**Use the `--init-image` flag to replace the default `vminitd` binary with a custom wrapper that executes boot-time logic before handing control to the real init process.**

The `apple/container` project runs each container inside a lightweight VM that boots with an init filesystem image containing the `vminitd` binary. By leveraging custom init images for container boot-time customization, you can inject arbitrary logic—such as logging, eBPF filter loading, or daemon startup—into the very first code that executes on the VM, before any user-provided container image starts.

## What Are Custom Init Images?

Each container runs inside a lightweight VM that boots with an **init filesystem image** providing the `vminitd` binary (the container’s init process). By default, this image is defined in the runtime configuration under the `[vminit]` section, typically pointing to `ghcr.io/apple/containerization/vminit:0.34.0`.

A **custom init image** is a small Linux filesystem containing a wrapper binary that replaces the original `vminitd`. This wrapper performs arbitrary boot-time work and then transfers control to the real `vminitd` via `exec`. This approach lets you instrument the VM bootstrap phase before the OCI container workload begins.

## How the `--init-image` Flag Works

The `--init-image` CLI flag overrides the default init image for a single run or create operation. Internally, the flag propagates through several layers of the Swift codebase:

- **Command-line parsing**: In [`Sources/Services/ContainerAPIService/Client/Flags.swift`](https://github.com/apple/container/blob/main/Sources/Services/ContainerAPIService/Client/Flags.swift), the `initImage` property captures the flag value between lines 177–268.
- **Runtime forwarding**: [`Sources/ContainerCommands/Container/ContainerRun.swift`](https://github.com/apple/container/blob/main/Sources/ContainerCommands/Container/ContainerRun.swift) forwards the flag to the container runtime service.
- **Image mounting**: [`Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift`](https://github.com/apple/container/blob/main/Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift) handles fetching and mounting the init filesystem before the OCI container starts.

The default unpack strategy for init images is provided by [`Sources/Services/ContainerAPIService/Server/SnapshotStore.swift`](https://github.com/apple/container/blob/main/Sources/Services/ContainerAPIService/Server/SnapshotStore.swift).

## Creating a Custom Init Image

Follow this workflow to build and deploy a custom init image that logs a message during VM boot.

### Step 1: Write the Wrapper Binary

Create a Go wrapper that writes to the kernel log and then executes the real `vminitd`. This example uses Go for easy cross-compilation.

```go
// wrapper.go – a 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: Build for the Target Architecture

Build the wrapper statically for the VM architecture (typically `arm64`).

```bash
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o wrapper wrapper.go

```

### Step 3: Create the Containerfile

Use a multi-stage build that preserves the original `vminitd` as `vminitd.real` and installs your wrapper as the new `/sbin/vminitd`.

```dockerfile

# Dockerfile that creates the custom init image

FROM ghcr.io/apple/containerization/vminit:0.34.0 AS base

FROM ghcr.io/apple/containerization/vminit:0.34.0

# Keep the original binary as vminitd.real

COPY --from=base /sbin/vminitd /sbin/vminitd.real

# Replace it with our wrapper

COPY wrapper /sbin/vminitd

```

### Step 4: Build the Custom Init Image

Build the image using the `container` CLI.

```bash
container build -t local/custom-init:latest .

```

### Step 5: Run with the Custom Init Image

Pass the `--init-image` flag to override the default init image for your container.

```bash
container run --name my-container \
    --init-image local/custom-init:latest \
    alpine:latest echo "hello"

```

### Step 6: Verify Boot-Time Execution

Inspect the VM boot logs to confirm your wrapper executed.

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

```

Expected output:

```

[    0.129230] custom-init: === CUSTOM INIT IMAGE RUNNING ===

```

## Key Implementation Details

The custom init image feature touches several critical files in the `apple/container` repository:

- **[`docs/how-to.md`](https://github.com/apple/container/blob/main/docs/how-to.md)** (lines 556–632): User-facing guide explaining the flag, wrapper creation, Dockerfile structure, build steps, and verification.
- **[`docs/command-reference.md`](https://github.com/apple/container/blob/main/docs/command-reference.md)** (lines 57–121): Formal CLI reference documenting the `--init-image` flag.
- **[`Sources/Services/ContainerAPIService/Client/Flags.swift`](https://github.com/apple/container/blob/main/Sources/Services/ContainerAPIService/Client/Flags.swift)** (lines 177–268): Defines the `initImage` property parsed from the command line.
- **[`Sources/ContainerCommands/Container/ContainerRun.swift`](https://github.com/apple/container/blob/main/Sources/ContainerCommands/Container/ContainerRun.swift)**: Core implementation forwarding `--init-image` to the runtime service.
- **[`Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift`](https://github.com/apple/container/blob/main/Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift)**: Handles fetching and mounting the init image before container startup.

## Summary

- **Custom init images** replace the default `vminitd` binary to inject logic into the VM boot sequence.
- Use the **`--init-image <image>`** flag to specify a custom init image for a single run or create operation.
- The wrapper binary must **`exec`** the real `/sbin/vminitd.real` after completing custom boot-time work.
- Build your wrapper for the target VM architecture (usually `arm64`).
- Verify execution using **`container logs --boot <name>`** to inspect early boot messages.

## Frequently Asked Questions

### What is the default init image if I don't specify `--init-image`?

According to the runtime configuration in the `[vminit]` section, the default image is `ghcr.io/apple/containerization/vminit:0.34.0`. This image provides the standard `vminitd` binary that initializes the VM environment before starting the container workload.

### Can I use any container image as a custom init image?

No, a valid custom init image must contain a compatible `vminitd` binary (or wrapper) that can act as the VM's init process. The image should derive from the official `vminit` base image to ensure compatibility with the VM kernel and versioning, as demonstrated in the Dockerfile example using `ghcr.io/apple/containerization/vminit:0.34.0` as both the base and final stage.

### How do I debug a custom init image that fails to boot?

Use the **`container logs --boot <container-name>`** command to inspect the VM boot logs. This output captures everything from the init process startup, including any output from your wrapper binary before it executes the real `vminitd`. Look for kernel messages or errors related to your custom init logic.

### Is the `--init-image` flag persistent across container restarts?

No, the `--init-image` flag applies only to the specific `run` or `create` operation where it is specified. It is not stored as part of the container configuration. To use a custom init image persistently, you must specify the flag every time you start or run the container.