Files
playbook/antigravity-awesome-skills/skills/apple-container/references/workflows.md
T
2026-07-10 16:04:20 +00:00

18 KiB

container — Task-Oriented Workflows

Copy-pasteable recipes for Apple's container CLI (apple/container) — Linux containers as lightweight VMs on Apple-silicon macOS. This is Apple's container, not Docker: flags, defaults, and the daemon model differ. See commands.md for the full flag reference. Use fully-qualified image refs (docker.io/library/alpine) in scripts. container targets macOS 26; several features are gated (called out inline).

Version: recipes track the 1.0.0 release. container cp, container prune, container image prune, and container system version used below were added in 1.0.0 (not in 0.7.1) — run container --help to confirm your installed surface.

Recipes


First run: install + start + first container

There is no CLI installer command — download and double-click the signed .pkg, then start the services and run a container.

# 1. Download the signed installer .pkg from the GitHub releases page:
#    https://github.com/apple/container/releases
#    Double-click the .pkg and follow the prompts (admin password installs under /usr/local).

# 2. Start the background services. On first run this prompts to install the default Linux kernel.
container system start

# 3. Verify (empty list is expected on a fresh install).
container list --all

# 4. Run your first container.
container run --rm -it docker.io/library/alpine:latest sh

Note: double-clicking the .pkg is the only install path; container system start prompts for the recommended kernel (or use container system kernel set --recommended).


Run interactive vs detached

# Interactive shell (-i keeps stdin open, -t allocates a TTY; commonly combined as -it).
container run --rm -it docker.io/library/ubuntu:latest /bin/bash

# Detached, named, auto-removed on stop.
container run -d --name web --rm docker.io/library/nginx:latest

# One-shot command in the foreground (exits when the command finishes).
container run --rm docker.io/library/alpine:latest uname -a

Note: foreground by default; stdin stays closed unless -i. --rm deletes the container after it exits.


Publish ports

-p / --publish forwards loopback traffic to the container. Spec: [host-ip:]host-port:container-port[/protocol] (tcp/udp, case-insensitive).

# host 8080 -> container 80
container run -d --rm -p 8080:80 docker.io/library/nginx:latest

# bind to a specific host IP
container run -d --rm -p 127.0.0.1:8080:8000 docker.io/library/node:latest npx http-server -a :: -p 8000

# IPv6 loopback (quote the bracketed address)
container run -d --rm -p '[::1]:8080:8000' docker.io/library/node:latest npx http-server -a :: -p 8000

# UDP
container run -d --rm -p 5353:5353/udp docker.io/library/alpine:latest

Note: if a container attaches to multiple networks, published ports forward to the IP of the first network's interface.


Set environment variables

# Individual vars (-e / --env); repeatable. Bare `KEY` inherits from the host.
container run --rm -e NODE_ENV=production -e PORT=8080 docker.io/library/node:latest env

# Inherit HOME from the host shell
container run --rm -e HOME docker.io/library/alpine:latest sh -c 'echo $HOME'

# From a file (KEY=value per line; # comments and blanks ignored); repeatable.
container run --rm --env-file ./app.env docker.io/library/alpine:latest env

Note: -e and --env-file may be combined and each repeated; later values win.


Mounts: bind, volume, tmpfs

# Bind-mount a host folder (-v host:container). Use absolute host paths.
container run --rm -v "${HOME}/Desktop/assets:/content/assets" docker.io/library/python:alpine ls -l /content/assets

# Same via --mount key=value syntax.
container run --rm --mount source="${HOME}/Desktop/assets",target=/content/assets docker.io/library/python:alpine ls /content/assets

# Named volume (create first, then mount by name).
container volume create data
container run --rm -v data:/var/lib/app docker.io/library/alpine:latest sh

# Anonymous volume (-v /path) — NOT auto-removed by --rm; delete it yourself.
container run --rm -v /data docker.io/library/alpine:latest sh
# It leaves an anon-<id> volume. List to find the exact name, then remove THAT one by name:
container volume list          # locate the new anon-<id> row
container volume rm anon-<id>  # ⚠️ delete the specific volume — do NOT `grep anon | rm`, which
                              #    would match (and destroy) every anonymous volume on the machine

# Read-only bind via --mount.
container run --rm --mount type=bind,source="${PWD}/conf",target=/etc/conf,readonly docker.io/library/alpine:latest cat /etc/conf/app.ini

# tmpfs (in-memory) mount; repeatable.
container run --rm --tmpfs /scratch docker.io/library/alpine:latest sh -c 'df -h /scratch'

Note: unlike Docker, anonymous volumes do not auto-clean with --rm — remove them via container volume rm.


Build an image from a Dockerfile

# Build from ./Dockerfile in the current context, tag as web-test.
container build -t web-test .

# Explicit Dockerfile path.
container build -f docker/Dockerfile.prod -t web-test:prod .

# Build, then run it.
container build -t web-test .
container run -d --name my-web-server --rm web-test

Note: with no -f, the builder looks for Dockerfile then Containerfile. The builder runs in its own VM (see resource-limits recipe to size it).


Multi-tag, build-args, no-cache, target

# Multiple tags on one build (-t repeatable).
container build -t my-app:latest -t my-app:v1.0.0 .

# Build-time variables (repeatable).
container build --build-arg NODE_VERSION=18 --build-arg ENV=prod -t my-app .

# Skip the build cache.
container build --no-cache -t my-app .

# Target a specific multi-stage build stage.
container build --target production -t my-app:prod .

# Multi-architecture image (arm64 + amd64 via Rosetta).
container build --arch arm64 --arch amd64 -t docker.io/me/web-test:latest .

Note: --arch is repeatable/comma-separated; --platform (os/arch[/variant]) takes precedence over --os/--arch.


Tag & push to Docker Hub / private registry

Registry commands live under the Image group (container registry, alias r). Credentials are stored in the macOS keychain.

# Log in (prompts for username + password, or use -u / --password-stdin).
container registry login docker.io
container registry login -u alice ghcr.io
echo "$TOKEN" | container registry login -u alice --password-stdin ghcr.io

# HTTP for a local registry.
container registry login --scheme http localhost:5000

# Tag the local image with a full registry reference.
container image tag web-test docker.io/alice/web-test:latest

# Push it (the final reference is printed on success).
container image push docker.io/alice/web-test:latest

# Private registry.
container image tag web-test registry.example.com/fido/web-test:latest
container image push registry.example.com/fido/web-test:latest

# List saved logins / log out.
container registry list   # 1.0.0+ (added after 0.7.1)
container registry logout ghcr.io

Note: the default registry is docker.io, so a bare web-test:latest resolves against Docker Hub. Change it via the registry.domain property (next recipe).


Set the default registry

# Inspect current properties (includes [registry] domain).
container system property list

Edit ~/.config/container/config.toml:

[registry]
domain = "registry.example.com"

Note: with registry.domain set, unqualified image names resolve against that host instead of docker.io.


Container-to-container networking

Each container gets its own IP on the default vmnet network. Read it from container ls or container inspect.

# Start a server, then find its IP.
container run -d --name my-web-server --rm web-test
container ls                       # IP column shows e.g. 192.168.64.3

# Scriptable IP lookup via inspect + jq.
container inspect my-web-server | jq -r '.[0].networks[0].address'

# Reach it from another container by IP (requires macOS 26 — see note).
container run --rm -it web-test curl http://192.168.64.3

Note: container-to-container access over the virtual network requires macOS 26 and does not work on macOS 15, where containers are isolated from one another at the network layer (see concepts.md §3) — a naming scheme cannot restore connectivity. The DNS domain in the next recipe resolves names host-side only (host → container); it does not enable container-to-container traffic on macOS 15.


Local DNS domain for name resolution

Register a local domain so <name>.<domain> resolves to a named container's IP. dns create/delete need sudo (writes under /etc/resolver).

# Create the domain (admin password required).
sudo container system dns create test

Make it the default DNS domain by setting dns.domain in ~/.config/container/config.toml (there is no property set command — properties are edited in the config file):

[dns]
domain = "test"
# Now a named container is reachable as <name>.<domain>.
container run -d --name my-web-server --rm web-test
curl http://my-web-server.test

# List / remove domains.
container system dns list
sudo container system dns delete test

# Confirm the effective domain.
container system property list        # shows [dns] domain = "test"

Note: container system property only supports list/ls — set dns.domain via config.toml, not a CLI setter. A container named my-web-server with domain test answers at my-web-server.test.


Custom isolated networks (macOS 26)

The entire container network group requires macOS 26 (absent on macOS 15, which has only the single default subnet).

# Create an isolated network.
container network create foo

# With custom subnets.
container network create foo --subnet 192.168.100.0/24 --subnet-v6 fd00:1234::/64

# Attach a container to it.
container run -d --name my-web-server --network foo --rm web-test

# List / delete (detach all containers first).
container network list
container stop my-web-server
container network delete foo

Note: networks are mutually isolated — a container on foo has no connectivity to containers on default.


Inspect a container

# Full pretty JSON (one or more IDs).
container inspect my-web-server

# Extract specific fields with jq.
container inspect my-web-server | jq -r '.[0].status'
container inspect my-web-server | jq -r '.[0].networks[0].address'

# Table/JSON listing of all containers, including stopped.
container ls --all
container ls --all --format json | jq '.[] | [.configuration.id, .networks[0].address]'

Note: inspect requires at least one ID; a missing ID errors. container image inspect <ref> does the same for images.


View logs

# Application stdio.
container logs my-web-server

# Follow live output.
container logs -f my-web-server

# Last N lines (-n is short-only; there is no --lines).
container logs -n 100 my-web-server

# VM boot / init logs.
container logs --boot my-web-server

Note: --boot shows the VM kernel + vminitd init log, useful for boot/networking diagnostics.


Exec into a running container

# Interactive shell in a running container.
container exec -it my-web-server sh

# One-off command.
container exec my-web-server ls /content

# With env / as a specific user.
container exec -e DEBUG=1 -u root my-web-server env

Note: exec targets a running container and shares the same process options as run (-e, --env-file, -u, -w, etc.).


Copy files in and out

Exactly one side must be a container_id:path reference. The container must be running.

# Host -> container.
container cp ./config.json my-web-server:/etc/app/

# Container -> host.
container cp my-web-server:/var/log/app.log ./logs/

Note: container copy and container cp are the same command.


Resource limits (CPU / memory)

Container defaults: 4 CPUs, 1 GiB RAM. Builder VM defaults: 2 CPUs, 2 GiB RAM.

# Per-container limits on run/create.
container run --rm --cpus 8 --memory 32g docker.io/library/alpine:latest sh

# Short flags.
container run --rm -c 2 -m 1G docker.io/library/node:latest

# Size the builder VM before a heavy build.
container builder start --cpus 8 --memory 32g
container build -t big-app .

# Change a running builder: stop, delete, restart with new limits.
# ⚠️ `builder delete` discards the BuildKit builder and its layer cache — the next build
#    re-fetches base images and rebuilds every layer from scratch.
container builder stop
container builder delete
container builder start --cpus 8 --memory 32g

Note: memory accepts K/M/G/T/P suffixes. Monitor live usage with container stats (--no-stream for a single snapshot).


Cleanup: stop, remove, prune

⚠️ Scope matters. The -a/--all/prune variants act on every container, image, volume, or network on the machine — not just this project's. container volume prune and container image rm --all permanently delete data belonging to unrelated containers. To clean up after one workload, use the targeted commands (by name/tag); reach for the machine-wide ones only when you deliberately want a full sweep. Do not paste this whole block blindly.

# --- Targeted (scoped to named resources — safe for cleaning up one workload) ---
container stop my-web-server            # stop one container
container delete my-web-server          # rm is an alias; add -f to remove a running one
container image delete web-test         # remove one image by name/tag
container volume rm data                # remove one volume by name

# --- Machine-wide (destroys ALL matching resources — use deliberately) ---
container stop -a                       # stop every running container
container rm --all                      # delete every container
container image rm --all                # delete every image
container image prune                   # remove dangling (untagged) images only
container image prune -a                # remove every image not used by a container
container volume prune                  # delete every volume with no container reference
container network prune                 # macOS 26+; delete every unused network
container prune                         # remove all stopped containers

# Check disk usage across images/containers/volumes.
container system df

Note: container prune only removes stopped containers. image prune (no -a) removes only dangling images.


Upgrade / downgrade / uninstall

Both scripts install to /usr/local/bin. Stop the services first.

# Always stop before upgrading/downgrading.
container system stop

# Upgrade to the latest release.
/usr/local/bin/update-container.sh

# Downgrade: uninstall (keep user data with -k), then pin the previous release.
/usr/local/bin/uninstall-container.sh -k
/usr/local/bin/update-container.sh -v 0.7.1   # replace with the version you want

# Restart after any change.
container system start

# Uninstall. Default to -k (keep user data) unless you truly want everything gone.
/usr/local/bin/uninstall-container.sh -k      # keep images/containers/volumes for a later reinstall
/usr/local/bin/uninstall-container.sh -d      # ⚠️ also deletes ALL user data (images, containers, volumes) — irreversible

Note: you can also upgrade/downgrade by re-downloading and double-clicking the signed .pkg. -v pins update-container.sh to a specific version.


Troubleshooting

# "XPC connection error" / commands hang -> services aren't running. Start them.
container system start

# Confirm services are up.
container system status

# View service logs (default last 5m; --last <n>[m|h|d], -f to follow).
container system logs
container system logs --last 1h
container system logs -f

# Add --debug to any command for verbose output (or set CONTAINER_DEBUG).
container --debug run --rm docker.io/library/alpine:latest true

# Version info for CLI + API server.
container system version

Note: an XPC/connection error means the background API server isn't started — run container system start. Feature gates to keep in mind: container-to-container access and the container network group require macOS 26 (both unavailable on macOS 15). The CLI refuses to run under Rosetta — Apple silicon only.