How to Manage Container Machines for Persistent Development Environments

Container machines in the Apple Container tool provide persistent, bootable Linux VMs that preserve state across sessions, mount your macOS home directory automatically, and support full-system services like systemd.

The apple/container repository provides a Swift-based utility for running OCI-compatible Linux containers as lightweight virtual machines on Apple Silicon Macs. When you manage container machines for persistent development environments, you create stable Linux workstations that survive reboots, maintain installed packages, and integrate seamlessly with your macOS file system and editors.

What Are Container Machines?

A container machine is a persistent, bootable Linux environment that runs as a lightweight VM on Apple Silicon. Unlike ephemeral containers, machines preserve their disk state, configuration, and running services across stops and restarts.

The architecture consists of three layers:

  1. OCI Image – Any OCI-compatible image containing /sbin/init can serve as a base. The image is fetched and stored, then instantiated as a VM disk.
  2. Container Runtime – The Swift package Containerization provides low-level VM, networking, and storage primitives, creating a lightweight VM backed by the image’s filesystem.
  3. CLI Front-end – The container machine commands, implemented in Sources/ContainerPlugin/, map user requests to runtime calls and handle JSON inspection and state persistence.

Key capabilities include:

  • Boot on demand – The first container machine run automatically starts the VM if it is not running.
  • Preserve state – The VM’s disk image, configuration, and any changes survive across stops and restarts.
  • Host integration – Your macOS user’s UID/GID and $HOME directory are automatically mounted inside the machine.
  • Full-system services – Because the image’s init system (such as systemd) runs inside the VM, you can start long-running daemons like PostgreSQL or Redis.

Creating and Starting Your First Machine

To create a new machine, use the container machine create command. This allocates a VM disk and writes a JSON configuration file to ~/.container/machines/<id>.json, as implemented in Sources/ContainerPlugin/PluginFactory.swift.


# Create but do not start yet

container machine create alpine:latest --name dev

# Run an interactive shell (auto-boots if needed)

container machine run -n dev

Inside the shell, $HOME points to your macOS home directory, giving seamless access to source files from macOS editors.

To avoid typing the machine name repeatedly, set a default:

container machine set-default dev
container machine run          # works on the default machine

The set-default command writes a symlink named default in the machines directory, handled by Sources/ContainerPlugin/LaunchPlist.swift.

Managing Machine Lifecycle and State

Machines persist their root filesystem in a qcow2-style image file inside your container data directory. When stopped, the VM’s memory is flushed, but the disk image remains untouched.

Stop a machine while preserving state:

container machine stop dev

The stop logic is implemented in Sources/ContainerPlugin/ServiceManager.swift, which sends a shutdown request to the VM and updates the configuration file.

Inspect machine details, including dynamic fields like IP address and PID:

container machine ls
container machine inspect dev | jq .

The inspect functionality reads the JSON config and queries the VM for runtime data, as defined in Sources/ContainerPlugin/PluginStateRoot.swift.

Configuring Resources and Defaults

Resize CPU and memory allocations using container machine set. Changes are persisted to the configuration file but only take effect after the next stop/start cycle.

container machine set -n dev cpus=4 memory=8G
container machine stop dev
container machine run -n dev -- nproc   # prints 4

This resource management logic resides in Sources/ContainerPlugin/PluginConfig.swift.

Building Custom Images with Systemd Support

You can bring your own image to manage container machines for specific development stacks. Any OCI image containing /sbin/init can serve as a machine base.

Here is a Dockerfile for an Ubuntu 24.04 development machine with systemd:

FROM ubuntu:24.04
ENV container container
RUN apt-get update && apt-get install -y \
    dbus systemd openssh-server net-tools iproute2 iputils-ping curl wget vim-tiny man sudo && \
    apt-get clean && rm -rf /var/lib/apt/lists/* && yes | unminimize
RUN >/etc/machine-id && >/var/lib/dbus/machine-id
RUN systemctl set-default multi-user.target

Build and instantiate the custom image:

container build -t local/ubuntu-machine:latest .
container machine create local/ubuntu-machine:latest --name ubuntu
container machine run -n ubuntu

Enabling Nested Virtualization

For advanced use cases, nested virtualization allows the VM to expose /dev/kvm to guest containers. This requires Apple Silicon M3 or later, macOS 15+, and a Linux kernel built with CONFIG_KVM=y.

Create a machine with a custom KVM-enabled kernel:

container machine create \
    --virtualization \
    --kernel /path/to/vmlinux-kvm \
    --name kvm-dev \
    alpine:latest

# Verify /dev/kvm is exposed inside

container machine run -n kvm-dev -- ls -l /dev/kvm

See docs/container-machine.md for complete details on nested virtualization requirements.

Summary

  • Container machines are persistent Linux VMs that maintain state across reboots, implemented in the apple/container Swift-based tool.
  • State is stored in qcow2 disk images and JSON configuration files located in ~/.container/machines/.
  • The CLI commands (create, run, stop, set, inspect) are implemented across PluginFactory.swift, ServiceManager.swift, PluginConfig.swift, and PluginStateRoot.swift.
  • Host integration automatically mounts your macOS $HOME and preserves UID/GID mappings.
  • Any OCI image with /sbin/init can serve as a machine base, enabling full systemd support and long-running daemons.

Frequently Asked Questions

Where is container machine state stored on macOS?

Machine state is stored in your user’s container data directory, typically ~/.container/machines/<id>.json for configuration metadata and a corresponding qcow2 disk image file for the root filesystem. These files persist across macOS reboots, allowing you to resume exactly where you left off.

Can I use any Docker image as a container machine base?

You can use any OCI-compatible image that includes /sbin/init as the entry point. Standard container images without an init system will not boot properly. For systemd-based distributions, ensure the image installs systemd and sets a default target, as shown in the Ubuntu Dockerfile example.

How do I enable nested virtualization for running VMs inside my container machine?

Nested virtualization requires Apple Silicon M3 or later, macOS 15 or later, and a Linux kernel compiled with CONFIG_KVM=y. Create the machine with the --virtualization and --kernel flags pointing to your KVM-enabled kernel binary. Once running, /dev/kvm will be available inside the guest for further virtualization.

Do resource changes apply immediately when I resize CPU or memory?

No. When you run container machine set to adjust CPUs or memory, the changes are written to the persistent configuration file but only take effect after you stop and restart the machine. The implementation in Sources/ContainerPlugin/PluginConfig.swift handles this persistence, while ServiceManager.swift applies the new limits on the next boot cycle.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →