Add container skill (#2154)

Signed-off-by: michael_crosby <michael_crosby@apple.com>
This commit is contained in:
Michael Crosby
2026-08-26 10:24:26 -07:00
committed by GitHub
parent d6de569420
commit d96dfa3126
6 changed files with 470 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
{
"name": "apple-container",
"description": "Skills for using container, Apple's tool for running Linux containers on macOS",
"owner": {
"name": "The container project maintainers"
},
"plugins": [
{
"name": "container",
"description": "Teaches Claude container's command surface and how it maps to Docker, Lima, Colima, and Podman on macOS",
"version": "0.1.0",
"source": "./"
}
]
}
+22
View File
@@ -0,0 +1,22 @@
{
"name": "container",
"description": "Teaches Claude container's command surface and how it maps to Docker, Lima, Colima, and Podman on macOS",
"version": "0.1.0",
"homepage": "https://github.com/apple/container",
"repository": "https://github.com/apple/container",
"license": "Apache-2.0",
"keywords": [
"container",
"containers",
"docker",
"lima",
"colima",
"podman",
"oci",
"macos",
"apple-silicon"
],
"skills": [
"./skills/container"
]
}
+39
View File
@@ -0,0 +1,39 @@
# Skills
Agent skills for working with `container`. A skill teaches a coding agent this tool's command
surface — how it maps to `docker`, `lima`, `colima`, and `podman`, and where it differs.
## container
Covers the full command surface, the Docker command mapping, and the differences that
commonly trip people up (singular command groups, `-t` on builds, the three-step DNS setup,
container machines).
## Install
Claude Code, from a local clone:
```bash
# in Claude Code, from anywhere
/plugin marketplace add /path/to/container
/plugin install container@apple-container
```
Or straight from GitHub, without a clone:
```bash
/plugin marketplace add apple/container
/plugin install container@apple-container
```
The skill loads on demand — it activates when a task involves containers, Dockerfiles, or
images on macOS, and stays out of the way otherwise.
## Editing
`skills/container/SKILL.md` is the always-loaded surface, so keep it short and put detail in
`skills/container/references/`.
Document command *names* and behavior that surprises people. Do not paste exhaustive flag
lists — they drift. `container <command> --help` is authoritative, and the skill tells the
agent to use it.
+92
View File
@@ -0,0 +1,92 @@
---
name: container
description: Use when running, building, or managing Linux containers on macOS, or when a task involves Docker, docker compose, Lima, Colima, or Podman commands on a Mac, Dockerfiles, OCI images, image registries, or setting up a Linux development environment on Apple silicon.
---
# container
`container` runs Linux containers on macOS (Apple silicon, macOS 26+), replacing Docker, Lima,
Colima, and Podman. It uses standard OCI images and ordinary Dockerfiles, so it pulls from and
pushes to any registry. You do not stand up or size a Linux host first: `container system
start`, then `container run`.
## Command groups are singular
`image`, `volume`, `network`, `registry`, `machine`, `system`. Docker's plurals do not exist —
`container images` is not a command, `container image ls` is.
**Never infer a subcommand from Docker. Run `container <group> --help` and use what it lists.**
Wrong commands here are plausible inventions, not typos.
An unrecognized subcommand falls through to the plugin loader, so it reports a *service*
problem rather than a naming one — `container images` prints `Error: Plugins are unavailable.
Start the container system services and retry`. Check the name before restarting anything; only
trust that message when `container system status` also reports the service down.
## Docker → container
| Docker | container |
|---|---|
| `docker ps` / `ps -a` | `container ls` / `ls -a` |
| `docker images` | `container image ls` |
| `docker pull` / `push` | `container image pull` / `push` |
| `docker rmi` | `container image rm` |
| `docker tag` / `save` / `load` | `container image tag` / `save` / `load` |
| `docker rm` | `container delete` (alias `rm`) |
| `docker login` / `logout` | `container registry login` / `logout` |
| `docker info` | `container system status` |
| `docker compose` | no equivalent — see `references/docker-migration.md` |
`run`, `build`, `exec`, `logs`, `cp`, `inspect`, `stats`, `start`, `stop`, `kill`, `export`,
`prune`, `volume *`, and `network *` match Docker, as do the common `run` flags: `-d`, `--rm`,
`-i`, `-t`, `-e`, `-p`, `-v`, `-w`, `--name`, `--network`, `--entrypoint`.
`restart`, `commit`, `attach`, `top`, `rename`, `pause`, `port`, and `--restart` have no
equivalent. Check `references/docker-migration.md` before assuming anything else exists.
## Gotchas
- **`container build` needs `-t`** — the default tag is a freshly generated UUID.
- **`container ls` hides stopped containers.** Use `-a`.
- **Every container gets its own IP**, reachable from the host and from other containers. `-p`
binds a host port; it is not needed for basic reachability. Get IPs from `container inspect`.
- **Builds run in a builder container.** If a build fails oddly, check `container builder
status`; `container builder start` takes `--cpus` and `--memory`.
- **Name resolution takes three steps, and works only on the `default` network:**
```bash
# 1. create ~/.config/container/config.toml containing:
# [dns]
# domain = "test"
container system stop && container system start # 2. reload the config
sudo container system dns create test # 3. point macOS at container's resolver
```
Then use `db.test` — a bare `db` never resolves. That file does not exist until you create
it, and it is TOML, so edit the `[dns]` table in place rather than appending a second one;
verify with `container system property ls`. There is no `container system dns default`
subcommand.
Containers on a network from `container network create` cannot resolve each other by name at
all ([apple/container#1809](https://github.com/apple/container/issues/1809)) — reach them by
IP. Custom networks are for isolation, not service discovery.
## Container machines
`container run` runs an application. A **container machine** is a Linux *environment* you work
inside: it boots the image's init system, and your username and home directory are mapped in,
so your repos and dotfiles are on both platforms at once.
```bash
container machine create alpine:latest --name dev
container machine run -n dev # interactive shell as your host user
```
Reach for this instead of Lima or Colima when you want a Linux shell rather than a single
containerized application. See `references/container-machines.md`.
## Reference
- `references/docker-migration.md` — full mapping, what has no equivalent, replacing compose
- `references/container-machines.md` — container machine workflows and host integrations
- `container <command> --help` — authoritative flags, always current
@@ -0,0 +1,120 @@
# Container machines
## What it is
`container run` runs an **application**. A **container machine** is a Linux **environment**
you work inside.
The difference that matters: a container machine boots the image's init system, so it can run
long-running services under a process supervisor, and it maps your macOS username and home
directory into Linux. Your repos and dotfiles are present on both platforms at the same time.
Edit with your macOS editor; compile and run inside Linux; point macOS-native profilers,
browsers, and GUI debuggers at the same files. There is no copy step between building
something and inspecting it.
When you want a Linux shell on your Mac rather than a single containerized application, this
is the tool — use it instead of Lima or Colima.
## Getting a shell
```bash
container machine create alpine:latest --name dev
container machine run -n dev # interactive shell as your host user
container machine run -n dev uname -a # one command, then exit
container machine run -n dev -- cat /proc/cpuinfo # use -- when the command takes flags
```
`container machine run` is the way in. It boots the container machine first if it is stopped.
You do not SSH into a container machine.
Inside, `whoami` returns your host username and `pwd` is your Mac home directory, mounted in.
## Setting a default
```bash
container machine set-default dev
container machine run # no -n needed
```
## Lifecycle
```bash
container machine ls # list
container machine inspect dev # JSON detail
container machine logs dev # boot and console logs
container machine stop dev
container machine rm dev # deletes its storage too
```
`container machine` has the alias `m`, so `m ls` and `m run` work.
## Sizing and the home mount
`--cpus` and `--memory` at create time; `container machine set` afterward. Memory defaults to
half of host memory. `--home-mount` takes `rw` (default), `ro`, or `none`.
```bash
container machine create ubuntu:24.04 --name dev --cpus 8 --memory 16G --set-default
container machine set -n dev cpus=4 memory=8G
container machine stop dev # set takes effect on the next boot
container machine run -n dev -- nproc
```
`container machine set` changes configuration on disk. It does **not** apply to a running
container machine — stop it, then `run` to reboot.
## Services under an init system
On an image with `systemd`, a container machine runs it, so system services work:
```bash
container machine run -n dev -- sudo systemctl start postgresql
container machine run -n dev -- systemctl status postgresql
```
This is the main reason to reach for a container machine over `container run` for a
development stack: dependencies live where a process supervisor manages them.
## One container machine per target distro
Each gets the same `$HOME` and the same dotfiles from your Mac, so testing across
distributions costs almost nothing:
```bash
container machine create alpine:latest --name alpine
container machine create ubuntu:24.04 --name ubuntu
container machine create debian:bookworm --name debian
```
## Custom images
Any Linux image with `/sbin/init` works. Build one like any other image:
```bash
container build -t local/ubuntu-machine:latest .
container machine create local/ubuntu-machine:latest --name ubuntu
```
For a `systemd` image, the Dockerfile needs `dbus` and `systemd` installed,
`systemctl set-default multi-user.target`, and several units masked
(`dev-hugepages.mount`, `sys-fs-fuse-connections.mount`, `systemd-update-utmp.service`,
`systemd-tmpfiles-setup.service`, `console-getty.service`). The complete working Dockerfile is
in the project's `docs/container-machine.md`.
## Nested virtualization
Requires Apple silicon M3 or later, macOS 15 or later, and a kernel built with `CONFIG_KVM=y`
— the default kernel does not have it.
```bash
container machine create --virtualization --kernel /path/to/vmlinux-kvm --name kvm-dev alpine:latest
container machine run -n kvm-dev -- ls -l /dev/kvm
```
Toggle on an existing container machine, then reboot it:
```bash
container machine set -n dev virtualization=true kernel=/path/to/vmlinux-kvm
container machine stop dev
container machine set -n dev kernel= # reset to the default kernel
```
@@ -0,0 +1,182 @@
# Docker → container migration
Complete command mapping. Verified against `container --help` and each group's `--help`.
When a flag matters, confirm with `container <command> --help` rather than assuming Docker's
spelling.
## Containers
| Docker | container | Notes |
|---|---|---|
| `docker run` | `container run` | |
| `docker create` | `container create` | |
| `docker start` | `container start` | |
| `docker stop` | `container stop` | |
| `docker kill` | `container kill` | |
| `docker rm` | `container delete` / `rm` | |
| `docker exec` | `container exec` | |
| `docker logs` | `container logs` | |
| `docker cp` | `container copy` / `cp` | `container:path` on either side |
| `docker export` | `container export` | |
| `docker inspect` | `container inspect` | also the way to find a container's IP |
| `docker stats` | `container stats` | |
| `docker ps` | `container list` / `ls` | add `-a` for stopped containers |
| `docker container prune` | `container prune` | |
| `docker restart` | — | `container stop <id> && container start <id>` |
| `docker attach` | — | use `container exec -it <id> sh` |
| `docker top` | — | `container exec <id> ps aux` |
| `docker port` | — | `container inspect <id>` |
| `docker commit` | — | build an image from a Dockerfile instead |
| `docker rename`, `pause`, `unpause`, `wait`, `diff`, `update` | — | no equivalent |
There is no `--restart` policy flag on `container run`. Supervise long-running services with
launchd on the host, or run them under an init system inside a container machine.
## Images
| Docker | container |
|---|---|
| `docker build` | `container build` |
| `docker images` | `container image list` / `ls` |
| `docker pull` | `container image pull` |
| `docker push` | `container image push` |
| `docker rmi` | `container image delete` / `rm` |
| `docker tag` | `container image tag` |
| `docker save` | `container image save` |
| `docker load` | `container image load` |
| `docker image inspect` | `container image inspect` |
| `docker image prune` | `container image prune` |
| `docker history` | — no equivalent |
`container image` has the alias `i`.
### Build notes
`container build` covers the BuildKit features people reach for most: `--platform`,
`--target`, `--build-arg`, `--secret`, `--no-cache`, `-f`, and `-o/--output` with
`type=oci|tar|local`.
Two differences worth knowing:
- `-t` is effectively required. The default tag is a freshly generated UUID, so a build
without `-t` produces an image you then have to hunt for in `container image ls`.
- Builds execute in a builder container managed by `container builder`. It starts on demand,
but when a build fails for no clear reason, check `container builder status`. Give it more
room with `container builder start --cpus 8 --memory 16g`.
## Registries, volumes, networks
| Docker | container |
|---|---|
| `docker login` / `logout` | `container registry login` / `logout` |
| — | `container registry list` — shows current logins |
| `docker volume create` / `ls` / `rm` / `inspect` / `prune` | `container volume create` / `list` / `delete` / `inspect` / `prune` |
| `docker network create` / `ls` / `rm` / `inspect` / `prune` | `container network create` / `list` / `delete` / `inspect` / `prune` |
| `docker network connect` / `disconnect` | — set `--network` when you run the container |
## System
| Docker | container |
|---|---|
| `docker info` | `container system status` |
| `docker version` | `container system version` |
| `docker system df` | `container system df` |
| daemon logs | `container system logs` |
| `docker events` | — no equivalent |
`docker system prune` has no single equivalent. Run the four prunes:
```bash
container prune # stopped containers
container image prune
container volume prune
container network prune
```
## Replacing a compose file
There is no `container compose`. A compose file becomes a shell script: DNS-resolvable names
on the `default` network, and `-d`.
**Do not reach for `container network create` here.** Name lookup between containers works on
the `default` network with a domain-qualified name. It does *not* work for containers on a
custom network — that gap is tracked as
[apple/container#1809](https://github.com/apple/container/issues/1809). A custom network is
for *isolating* containers; if you use one, wire the containers together by IP from
`container inspect <name>`, not by name.
Set up name resolution once (all three steps — see SKILL.md):
```bash
# [dns] domain = "test" in ~/.config/container/config.toml
container system stop && container system start
sudo container system dns create test
```
Then translate the file. This compose file:
```yaml
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
web:
image: my-app:latest
ports: ["8080:80"]
environment:
DATABASE_URL: postgres://postgres:secret@db:5432/postgres
depends_on: [db]
```
becomes:
```bash
#!/bin/bash
set -euo pipefail
# no --network flag: both containers land on `default`, where name lookup works
container run -d --name db \
-e POSTGRES_PASSWORD=secret \
postgres:16
# depends_on becomes an explicit readiness check
until container exec db pg_isready -q; do sleep 1; done
# db.test, not db — the name must be domain-qualified
container run -d --name web -p 8080:80 \
-e DATABASE_URL=postgres://postgres:secret@db.test:5432/postgres \
my-app:latest
```
Teardown:
```bash
container stop web db
container delete web db
```
Mapping notes:
- `depends_on` has no declarative form. Compose only waits for *start*, not readiness, so an
explicit readiness loop is usually more correct than what it replaced.
- Reference other services as `<name>.<domain>` (`db.test`). A bare `db` does not resolve.
This means editing your application's config, not just the `run` command.
- `ports:``-p`. Often unnecessary between containers, since each container has its own IP
and is reachable without publishing. You need `-p` to reach a service from a host browser or
a macOS-native tool.
- `volumes:``-v` for bind mounts, or `container volume create` plus `-v <name>:<path>`.
- `build:` → a `container build -t <name> .` line before the `run`.
- `restart:` has no equivalent. Supervise with launchd on the host, or run the stack under an
init system inside a container machine.
## Features with no Docker counterpart
- `container machine` — a full Linux environment with your home directory mapped in. See
`container-machines.md`.
- `container run --publish-socket <host_path:container_path>` — publish a Unix socket to the
host rather than a TCP port.
- `container run --ssh` — forward your SSH agent socket into the container.
- `container run --virtualization` — expose virtualization to the container for nested use.
- `container run --rosetta` — run x86-64 binaries on Apple silicon.
- `container system kernel` — manage the kernel containers boot with.