1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
|
---
name: apple-container-machine
description: >
Use this skill when working with Apple's `container machine` subcommand on
Apple Silicon Macs — creating persistent Linux environments, building custom
machine images, managing lifecycle, setting up VSCode Remote-SSH development,
or configuring host integration (home-dir mount, SSH, DNS). Triggers on
"container machine", "container m", "machine create", "machine run",
"apple container machine", "container machine image", "m run", or any
request involving Apple's container tooling for persistent Linux environments.
---
# Apple Container Machine Skill
## What This Is
`container machine` (alias `m`) is a subcommand of Apple's [container](https://github.com/apple/container) tool — an open-source CLI that runs Linux containers as lightweight virtual machines on Apple Silicon Macs (macOS 26+). Unlike standard `container run` which models an application container, `container machine` models a **persistent Linux workstation environment**: it runs the image's init system (`/sbin/init`), maps your macOS user account and home directory into the VM, and survives stop/start cycles.
**Key differences from `container run`:**
- Runs `/sbin/init` (supports `systemd`, `openrc`, etc.) for long-running services
- Auto-maps host username, UID, GID, and `$HOME` into the VM
- Persistent filesystem across stop/start — data survives
- Resources (CPU, memory) configurable at create time and resizeable later
- One machine per target distro — test the same app under Alpine, Ubuntu, Debian
## Prerequisites
- Apple Silicon Mac (M1+)
- macOS 26 recommended (macOS 15 has limitations: no container-to-container networking, no `container network` commands)
- `container` tool installed and `container system start` has been run
- Linux kernel installed (`container system kernel set --recommended`)
## Quickstart
```bash
# Create a container machine from an OCI image
container machine create alpine:3.22 --name dev
# Run a command (boots automatically if stopped)
container machine run -n dev whoami # your host username, not root
container machine run -n dev pwd # your Mac home directory, mounted in
# Open an interactive shell
container machine run -n dev # login shell as your host user
# List all machines
container machine ls
# Stop and delete
container machine stop dev
container machine rm dev
```
The alias `m` works everywhere `machine` does: `m ls`, `m run -n dev`, `m stop dev`.
## Container Machine Lifecycle
```
pull image → create → boot → run commands / services → stop → delete
```
### Create
```bash
container machine create <image> --name <name> [--cpus N] [--memory N] [--home-mount rw|ro|none]
```
The image must contain `/sbin/init`. See "Image Requirements" below.
- `--no-boot` — create without booting
- `--set-default` — also set as the default machine
- `--cpus` — virtual CPUs (default: half of host count)
- `--memory` — RAM (default: half of host memory, e.g., `8G`, `16G`)
- `--home-mount` — home directory access: `rw` (default, read-write), `ro` (read-only), `none` (no mount)
### Boot
A machine boots automatically on first `container machine run` or when `run` is invoked on a stopped machine. No separate boot command is needed.
### Run Commands
```bash
container machine run -n <name> [command...] # run a single command and exit
container machine run -n <name> # interactive login shell
container machine run -n <name> -- <command...> # pass args after --
container machine run -n <name> --root # run as root instead of host user
container machine run -n <name> --detach <command> # run in background
```
**Quoting limitation.** `container machine run` wraps the command in a shell layer that re-interprets quoting. Complex shell constructs — `&&` chaining, heredocs (`<<`), pipes to the command, mixed quote styles — are unreliable. Use simple single commands; for multi-step work, write a script on the mounted home dir and invoke it:
```bash
# Reliable: simple command
container machine run -n sdamin -- cat /etc/os-release
# Unreliable: this will likely mangle quoting
container machine run -n sdamin -- sh -c "systemctl status nginx && journalctl -u nginx -n 5"
# Reliable workaround: source the script from the mounted home
container machine run -n sdamin -- bash ~/projects/scripts/check.sh
```
Process flags: `-e KEY=value`, `--env-file`, `-u user`, `--uid`, `--gid`, `-w /workdir`, `-i` (interactive), `-t` (tty).
### Inspect
```bash
container machine inspect <name> # JSON detail
container machine ls # table: name, IP, cpus, memory, disk, state
container machine logs <name> # container machine stdio logs
container machine logs --boot <name> # VM boot log
```
### Stop
```bash
container machine stop <name> # graceful stop
```
### Configure (resize resources)
```bash
container machine set -n <name> cpus=4 memory=8G
container machine stop <name>
container machine run -n <name> # restart for changes to take effect
```
### Set Default
```bash
container machine set-default <name> # drop the -n flag for subsequent commands
```
### Delete
```bash
container machine stop <name>
container machine rm <name> # removes machine AND persistent storage
```
If the deleted machine was the default, set a new one: `container machine set-default <new-name>`.
## Image Requirements
Container machines require OCI images that include `/sbin/init`. The machine runs the image's init system as PID 1 on boot.
### Ubuntu 24.04 + systemd (canonical example)
This Dockerfile builds a machine image with systemd and common tools:
```dockerfile
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
RUN >/var/lib/dbus/machine-id
RUN systemctl set-default multi-user.target
RUN systemctl mask \
dev-hugepages.mount \
sys-fs-fuse-connections.mount \
systemd-update-utmp.service \
systemd-tmpfiles-setup.service \
console-getty.service
RUN systemctl disable \
networkd-dispatcher.service
RUN sed -i -e 's/^AcceptEnv LANG LC_\*$/#AcceptEnv LANG LC_*/' /etc/ssh/sshd_config
```
Build and create:
```bash
container build -t local/ubuntu-machine:latest -f Dockerfile .
container machine create local/ubuntu-machine:latest --name ubuntu
```
### systemd on other distros
The same pattern applies to any systemd-based distro (Debian, Fedora, Rocky Linux):
- Set `ENV container container`
- Install `dbus` and `systemd`
- Create empty `/etc/machine-id` and `/var/lib/dbus/machine-id`
- Set `multi-user.target` as default
- Mask unnecessary services
### init systems other than systemd
Any Linux image with `/sbin/init` works. For Alpine (busybox-init) or distros with OpenRC, the init system handles PID 1 responsibilities.
### First-boot provisioner
By default, `container` runs a built-in setup script on first boot to create the user matching your host account. To use your own setup instead, add an executable script at `/etc/machine/create-user.sh` to the image. It runs once, as root, on first boot, with these environment variables:
- `CONTAINER_GID` — group ID
- `CONTAINER_HOME` — home directory path
- `CONTAINER_MACHINE_ID` — machine UUID
- `CONTAINER_UID` — user ID
- `CONTAINER_USER` — username
## Commands Reference
`m` is the alias for `machine` — both forms work everywhere.
| Command | Description |
|---------|-------------|
| `container machine create <image>` | Create and boot a machine. Flags: `--name`, `--cpus`, `--memory`, `--home-mount`, `--no-boot`, `--set-default`, `--scheme`, `--progress` |
| `container machine run` | Run a command or open a shell in a machine (boots if stopped). Flags: `-n <name>`, `-d` (detach), `--root`, process flags (`-e`, `-u`, `-w`, `-i`, `-t`, `--gid`, `--uid`, `--env-file`) |
| `container machine ls` | List machines. Flags: `--format json\|table`, `-q` |
| `container machine inspect <name>` | JSON detail of a machine |
| `container machine set <key=value...>` | Update config. Flags: `-n <name>`. Valid keys: `cpus`, `memory`, `home-mount`. Requires stop/start. |
| `container machine set-default <name>` | Set the default machine |
| `container machine stop <name>` | Stop a running machine |
| `container machine logs <name>` | Show logs. Flags: `--boot`, `-f` (follow), `-n N` |
| `container machine rm <name>` | Delete machine and persistent storage |
## Host Integration
### Home directory mount
The container machine automatically mounts your macOS home directory inside the VM at `/Users/<username>`. Your repos, dotfiles, and SSH keys are available from both macOS and the Linux environment.
```bash
container machine create ... --home-mount rw # read-write (default)
container machine create ... --home-mount ro # read-only
container machine create ... --home-mount none # no mount
```
- The host user is automatically created in the machine with matching UID/GID
- Edit on macOS with your native tools; compile and run in the Linux VM
- Profilers, screenshot tools, and debuggers on macOS see the same files
**Important: `$HOME` path mismatch.** The machine sets `$HOME` to `/home/<username>` inside the VM, but the actual mounted home is at `/Users/<username>`. Tools that resolve `~` relative to `$HOME` (SSH key lookup, config file discovery) may look in the wrong place. This most commonly breaks SSH identity file resolution. Workaround:
```bash
# Symlink the VM home .ssh to the mounted macOS .ssh
container machine run -n <name> --root ln -s /Users/<username>/.ssh /home/<username>/.ssh
```
Or use absolute paths when referencing files on the mounted home.
### SSH agent forwarding
Add an SSH config entry so the machine can access your SSH agent:
```bash
cat >> ~/.ssh/config <<EOT
Host *.machine
ForwardAgent yes
UserKnownHostsFile /dev/null
EOT
```
### DNS domain setup
Create a local DNS domain so you can reach machines by hostname:
```bash
sudo container system dns create machine
```
After this, `ping <machine-name>.machine` resolves to the machine's IP:
```bash
ping -c 1 ubuntu.machine
```
To remove the domain later:
```bash
sudo container system dns delete machine
```
### Passwordless sudo inside a machine
Services started via `systemctl` inside the machine run as root by default. For interactive sudo access:
```bash
container machine run -n <name> sudo passwd $(whoami)
# Enter a password, then use sudo normally in shells
```
## VSCode Remote-SSH Development
This workflow lets you use Visual Studio Code with the Remote-SSH extension to develop code stored on your Mac, compiled and run inside the container machine.
### Setup
1. Create a machine with systemd and SSH:
```dockerfile
FROM ubuntu:24.04
ENV container container
RUN apt-get update && \
apt-get install -y \
dbus systemd openssh-server sudo curl wget git && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN >/etc/machine-id
RUN >/var/lib/dbus/machine-id
RUN systemctl set-default multi-user.target
RUN systemctl mask \
dev-hugepages.mount \
sys-fs-fuse-connections.mount \
systemd-update-utmp.service \
console-getty.service
```
2. Build and create:
```bash
container build -t ubuntu-machine:latest .
container machine create --set-default --name ubuntu ubuntu-machine:latest
```
3. Set up DNS:
```bash
sudo container system dns create machine
```
4. Add SSH config:
```bash
cat >> ~/.ssh/config <<EOT
Host ubuntu.machine
HostName ubuntu.machine
ForwardAgent yes
UserKnownHostsFile /dev/null
EOT
```
5. Set a password for SSH:
```bash
container machine run -it sudo passwd $(whoami)
```
### Connect
1. In VSCode, press Cmd-Shift-P and run **Remote-SSH: Connect to Host**
2. Select `ubuntu.machine`
3. Accept the SSH fingerprint
4. Enter the password you configured
5. In the new window, install any language extensions (Swift, Python, Rust, etc.) in the remote context
Your macOS home directory is mounted at `/Users/<username>` inside the machine — all your repos are available.
### Debugging
Set breakpoints in VSCode, run the application inside the machine, and debug normally. The home-dir mount means compiled artifacts are visible to both the machine and macOS tools.
### Clean up
```bash
container machine stop ubuntu
container machine rm ubuntu
container image rm ubuntu-machine:latest
sudo container system dns delete machine
# Remove the SSH config entry from ~/.ssh/config
```
## Resource Configuration
### At create time
```bash
container machine create alpine:3.22 --name dev --cpus 4 --memory 8G
```
- Default CPUs: half of host count
- Default memory: half of host memory
- Memory format: `512M`, `2G`, `8G`, etc. (1 MiB granularity)
### Post-creation resize
```bash
container machine set -n dev cpus=8 memory=16G
container machine stop dev
container machine run -n dev -- nproc # verify: 8
```
Changes only take effect after a stop/start cycle.
### Check current resources
```bash
container machine inspect dev | jq '.cpus, .memoryInBytes'
container machine ls # table shows CPUS and MEMORY
```
## Multi-Distro Workflows
Create one container machine per target distribution. Each machine mounts the same `$HOME`, so your repos and dotfiles are available everywhere:
```bash
container machine create alpine:3.22 --name dev-alpine
container machine create ubuntu:24.04 --name dev-ubuntu # uses /sbin/init from systemd
container machine create debian:latest --name dev-debian
```
```bash
container machine run -n dev-alpine apk add build-base
container machine run -n dev-ubuntu apt-get install build-essential
container machine run -n dev-debian apt-get install build-essential
```
Each machine runs its own init system under its own Linux VM, fully isolated. Test your application under different distributions and init systems from the same source tree.
## Troubleshooting
### Machine fails to boot
Ensure a Linux kernel is installed:
```bash
container system kernel set --recommended
```
Check boot logs:
```bash
container machine logs --boot <name>
```
### DNS not resolving (`<name>.machine` unknown)
Verify the DNS domain was created:
```bash
cat /etc/resolver/machine
sudo container system dns create machine
```
Verify the machine is running and has an IP:
```bash
container machine ls
```
### SSH connection refused
Ensure SSH is installed and running inside the machine:
```bash
container machine run -n <name> sudo systemctl status ssh
container machine run -n <name> sudo systemctl enable --now ssh
```
On images without systemd, start sshd explicitly:
```bash
container machine run -n <name> sudo /usr/sbin/sshd
```
Check SSH config:
```bash
ssh -v ubuntu.machine
```
### Home-mount permission errors
If the machine uses `--home-mount ro`, writes to `$HOME` will fail. Switch to `rw`:
```bash
container machine set -n <name> home-mount=rw
container machine stop <name>
container machine run -n <name>
```
### macOS 15 limitations
On macOS 15, the following do not work:
- Container-to-container networking over the virtual network
- `container network` commands
- `--network` flag on `container run`/`container create`
Container machines still work but are limited to host-only networking.
### Resource not updating after `container machine set`
Changes to CPU/memory/home-mount only apply after a stop/start cycle:
```bash
container machine set -n dev cpus=4
container machine stop dev # must run after set
container machine run -n dev # boot with new config
```
### Machine appears but is unreachable
Rare vmnet subnet conflict on macOS 15. If your machine has no network access:
1. Stop the machine: `container machine stop <name>`
2. Stop the container system: `container system stop`
3. Start again: `container system start`
4. Boot the machine: `container machine run -n <name>`
## Common Pitfalls
1. **macOS 15 network isolation**: Container-to-container communication does not work on macOS 15. Only the host can reach the container's IP.
2. **No default kernel**: `container system start` prompts to install one automatically. If skipped, run `container system kernel set --recommended`.
3. **SSH host key changes**: After deleting and recreating a machine with the same DNS name, `ssh` will warn about host key change. Remove the old key from `~/.ssh/known_hosts`.
4. **Home-mount read-only**: `--home-mount ro` causes any write to `$HOME` to fail. Use `rw` unless you have a specific security reason.
5. **Forgetting to stop before delete**: `container machine rm` stops the machine first, but data loss is still permanent.
6. **Images without `/sbin/init`**: A container machine will not boot from a standard application image (e.g., `python:alpine`). Use images built with `/sbin/init`.
7. **Disk space**: Each machine has its own persistent disk. Monitor usage with `container system df`.
8. **Multiple machines sharing the same home directory**: Concurrent writes from multiple machines to the same files can cause conflicts. Use different working directories per machine.
9. **`$HOME` is not `/Users/<user>`**: The machine sets `$HOME` to `/home/<user>` but the mounted macOS home is at `/Users/<user>`. SSH identity file paths in `~/.ssh/config` resolve relative to `$HOME` and will point to `/home/<user>/.ssh/` where keys don't exist. Symlink `/home/<user>/.ssh` to `/Users/<user>/.ssh` to fix.
10. **Shell quoting with `container machine run`**: Avoid `&&`, heredocs, and pipes in commands passed to `container machine run`. The command wrapper re-interprets quoting. Use single commands or invoke scripts from the mounted home directory instead.
## Sources
- [Apple container GitHub repo](https://github.com/apple/container)
- `docs/container-machine.md` — full container machine guide
- `docs/command-reference.md` — Machine section with all flags
- `docs/how-to.md` — container machines how-to
- `examples/container-machine-vscode/README.md` — VSCode Remote-SSH walkthrough
|