- Dockerfile 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
MAJOR is the Debian major, MINOR the snapshot date, PATCH the release serial on one snapshot; release.yaml refuses a version whose first two parts disagree with Dockerfile's FROM. The pin moves to pin.yaml and only commits the Dockerfile: CHANGELOG.md is not written by a workflow, so a new snapshot ships with the next entry. Overlays build FROM the base version being released rather than latest. |
||
| .claude-plugin | ||
| .forgejo/workflows | ||
| skills/apple-container | ||
| CHANGELOG.md | ||
| Dockerfile | ||
| jdk25.Dockerfile | ||
| kind.Dockerfile | ||
| podman.Dockerfile | ||
| README.md | ||
machine-debian
A base image for Apple container machines — long-lived Debian/systemd
Linux VMs on macOS (Apple Silicon). It ships a general development baseline;
each project overlays only its own non-standard tooling and boots a
project-specific machine from the result.
Published as sarak.sh/mikluko/machine-debian.
This repo is also a Claude Code plugin shipping the apple-container skill —
the working knowledge behind this project (see Claude plugin).
Requirements
- macOS on Apple Silicon.
- Apple's
containerCLI, v1.0+ (container --version). - arm64 only. The image is built and published for arm64.
Tags
The base and each overlay are separate packages, released together under one
version from CHANGELOG.md:
- MAJOR is the Debian major version of the base:
13for trixie. - MINOR is the date of the Debian snapshot,
YYYYMMDD. - PATCH counts releases on one snapshot.
13.20261027.1 is the second release on debian:trixie-20261027. Each release
publishes MAJOR.MINOR.PATCH, MAJOR.MINOR, MAJOR and latest per package.
Pin the full version for reproducibility.
- base:
sarak.sh/mikluko/machine-debian - kind:
sarak.sh/mikluko/machine-debian/kind - jdk25:
sarak.sh/mikluko/machine-debian/jdk25 - podman:
sarak.sh/mikluko/machine-debian/podman
Releasing
release.yaml runs on a push to main that
touches CHANGELOG.md. When the newest entry names a version no tag names yet,
it builds and pushes every image, then creates the vMAJOR.MINOR.PATCH tag and
release. It refuses a version whose MAJOR and MINOR disagree with the snapshot
Dockerfile pins.
Base pinning
Dockerfile's FROM tracks a dated Debian snapshot (debian:trixie-YYYYMMDD),
not the floating trixie. Daily, pin.yaml
resolves Debian's newest trixie-YYYYMMDD tag and commits the new pin when it
changes. A pin publishes nothing by itself; it ships with the next release,
MAJOR.YYYYMMDD.0 of the new snapshot.
Setup: the registry login exchanges the workflow's OIDC token through an
Authorized Integration of the owner's with scope write:package. The
repository variable REGISTRY_AUDIENCE holds that integration's audience.
What's in the base
- Init: systemd as PID 1 (
ENTRYPOINT /sbin/init), so services run normally inside the machine. - Languages: Go, Node.js + npm.
- Toolchain:
build-essential, Git, OpenSSH client,sudo,ca-certificates. - DNS: a local unbound on
127.0.0.1, forwarding over DNS-over-TLS to1.1.1.1and9.9.9.10, is the machine's only resolver; the vmnet gateway the machine is handed at boot is not used.
No container runtime is baked into the base. Everything beyond this baseline, including Docker or Podman, is a per-project or shared overlay (see Overlays).
How it works
- This repo builds and publishes the base image to
sarak.sh/mikluko/machine-debian. - A project defines its own
DockerfilewithFROM sarak.sh/mikluko/machine-debianand adds project-specific packages/tools (see Overlays). - The project builds that Dockerfile into a local image and creates a
container machinefrom it. The machine is a persistent VM youruncommands in,stop, anddelete.
A container machine boots from an image snapshot. Rebuilding the image does
not update a running machine — to pick up changes you rebuild and re-create
the machine (see Update).
Playbooks
Sizing used below: --cpus 6 --memory 16G. Adjust per project.
Publish / update the base image
Run from this repo. Builds arm64 and pushes to sarak.sh.
container registry login sarak.sh # once
container build -t sarak.sh/mikluko/machine-debian .
container image push sarak.sh/mikluko/machine-debian
Create a project machine
Run from the project directory (the one with the FROM sarak.sh/mikluko/machine-debian Dockerfile). Replace myproject.
container build -t machine-myproject .
container machine create machine-myproject --name myproject --cpus 6 --memory 16G
create boots the machine. Confirm:
container machine ls
Enter / run in a machine
The host-matched default user can't reach the root-owned Docker socket, so
docker fails. Fix it by joining the docker group with --gid (least
privilege; avoids the root home-mount hazard). Detect the gid inside the machine
— getent group docker needs no root:
gid=$(container machine run -n myproject -- getent group docker | cut -d: -f3)
container machine run -n myproject --gid "$gid" -t -i # interactive shell
container machine run -n myproject --gid "$gid" -- <cmd> [args] # one-off command
There is no machine-level default-user setting, so pass --gid on every run
(see Gotchas). run boots the machine if it is stopped.
Update a project machine
Machines cannot hot-swap their image, so re-create:
container build -t machine-myproject . # rebuild (pull fresh base first if needed)
container machine stop myproject
container machine delete myproject
container machine create machine-myproject --name myproject --cpus 6 --memory 16G
To also pick up a newer base image, pull it before rebuilding:
container image pull sarak.sh/mikluko/machine-debian
Delete a project machine
container machine stop myproject
container machine delete myproject
container image delete machine-myproject # optional: drop the local image too
Gotchas
- Use the
dockergroup, not--root.rundefaults to the host-matched user, who can't reach the root-owned Docker socket. Join the group with--gid(detect it viagetent group docker, no root needed). Docker's daemon runs as root and stores under machine-local/var/lib/docker, so the group is enough — no rootless/home-mount storage problem. There is no machine-level default-user setting, so pass--gidon everyrun. (Rootless Podman is daemonless and does hit the home-mount storage issue below — run it as root or point its storage at a machine-local path.) - Your host
$HOMEis mounted read-write into the machine at the same path, andrundefaults its working directory into it. A relative write inside the machine can hit your real host files. Build and scratch in machine-local paths (/root,/var/tmp), not under the mounted home. - SSH agent forwarding is transparent for 1Password — its agent socket lives
under
~/Library/...(inside the mounted home), sossh-add -lworks in the machine with no setup. A stock macOSssh-agentsocket lives outside$HOMEand would not forward this way. container machine runoften prints a spurious first lineError: The operation couldn't be completed. Operation not supported by device. It is harmless; the command still runs.container machine createmay print an XPC boot timeout yet boot fine — checkcontainer machine lsbefore retrying.
Overlays
Common tool layers ship as named overlay Dockerfiles here. Each is FROM sarak.sh/mikluko/machine-debian, inherits the base init/entrypoint, and CI
publishes it as its own sub-package (see Tags).
kind.Dockerfile→sarak.sh/mikluko/machine-debian/kind—kind+ Podman as its container provider (KIND_EXPERIMENTAL_PROVIDER=podman).jdk25.Dockerfile→sarak.sh/mikluko/machine-debian/jdk25— OpenJDK 25 (headless) + Maven (from Debian trixie-security) + Docker Engine.podman.Dockerfile→sarak.sh/mikluko/machine-debian/podman— rootful Podman serving the Docker-compatible API on port 2375, for a host client that speaks to Docker, such as an IDE's Docker interpreter, attcp://<name>.machine:2375. A machine publishes no ports and changes address on every boot, so the client goes by name, which resolves oncesudo container system dns create machinehas run. Unauthenticated, as root.
Create a machine straight from a published overlay, no local build:
container machine create sarak.sh/mikluko/machine-debian/kind \
--name myproject --cpus 6 --memory 16G
Or build an overlay Dockerfile locally (e.g. to pin a version or tweak it):
container build -t machine-myproject -f kind.Dockerfile .
container machine create machine-myproject --name myproject --cpus 6 --memory 16G
A project needing several overlays composes them in its own Dockerfile: start
FROM sarak.sh/mikluko/machine-debian and copy in the RUN blocks it wants
(a single base per image, so overlays are combined by hand, not chained).
Claude plugin
This repo is a Claude Code plugin shipping the apple-container skill: the
accumulated working knowledge of container machine (the gotchas around home
mounts, rootless storage, SSH-agent forwarding, and systemd/Docker inside the
VM). It is published through the
mikluko/marketplace-claude
marketplace:
/plugin marketplace add mikluko/marketplace-claude
/plugin install machine-debian@mikluko
Plugin layout: .claude-plugin/plugin.json (manifest), skills/apple-container/.