Beta 5: Apple container pools, Docker inside containers, and x86_64 runners
Tuesday, November 3, 2026 · Alex Rabin
kinhin watches your CI queue and, for each queued job, starts a fresh VM or container on your Apple Silicon Mac, registers a one-job runner, lets it run, then deletes it. By default nothing idles and nothing is left over; the exceptions are runners you keep with min_idle and the Docker Hub cache described below. It is free, open source (Apache-2.0), and sends no telemetry unless you point it at your own OpenTelemetry server.
Beta 5 is mostly about Linux runners on Apple container. You can now split them into named pools, run Docker inside them, run x86_64 images under Rosetta, and keep idle runners registered. It has no breaking change. This post covers what is new, what to do when you upgrade, what has been run, and what has not.
What is new
Named container pools
container.pools: splits Apple container into pools. Each pool has its own image, cpus, memory_gb, labels, min_idle, docker, flags and an optional max:; the container: section's own keys are the defaults. A job goes to the pool whose labels it carries, and runs-on: [..., kinhin-container-NAME] targets one pool. Pools share Apple container's CPU and memory, and a runner starts only when its pool's size fits what is left.
kinhin image prepare --container --pool NAME(or--all) bakes a pool's image, andkinhin workflow route --pool NAMEshows where a job would go.- A pool whose image is not built is left out, so its jobs wait instead of holding every pool in spawn backoff.
kinhin doctorlists each pool, its image and whether it fits.- The app shows and edits pools: Advanced → Routing has a card per pool and Add Container Pool…, Edit… and Remove, and the Fleet tab shows each container runner's pool.
- Telemetry labels Apple container series with
kinhin.container.pooland reports an unbuilt pool askinhin.spawn.held.
Without pools:, nothing changes. See Several container pools.
Docker inside container runners
container.docker: true starts dockerd in each Apple container runner before it registers, so jobs can docker run, docker build and docker compose. kinhin image prepare --container installs Docker when the base image lacks it, and container.flags: passes extra arguments to container run.
The container gets every Linux capability to run Docker, so keep this on pools you trust the workflows of. See Docker inside container runners.
A Docker Hub cache
With docker: true, Docker Hub pulls go through a pull-through cache that kinhin runs: one Apple container, registry-cache-kinhin, started by the first Docker runner. A job's services: and Docker actions are pulled from Docker Hub once per Mac instead of once per job, under one anonymous pull limit. If the cache cannot be reached, the runner registers anyway and pulls from Docker Hub. It is on by default; docker: { cache: false } turns it off. kinhin doctor reports it. See Docker Hub cache.
x86_64 runners under Rosetta
container.platform: linux/amd64, on the section or on one pool, runs x86_64 Linux runners on Apple container. The image is baked as amd64, the runner registers with x64 instead of arm64, and a job that asks for x64 goes to an amd64 pool. Docker jobs on an amd64 runner build through a buildx builder, because dockerd's built-in builder cannot run a build step under Rosetta. See x86-64 container runners.
Idle runners that stay registered
min_idle keeps a number of registered, idle runners for each pool and each Tart image, so the forge's runner list always shows a free runner for every label set the fleet serves. Set it on an images: entry, on container: or docker:, or use image_min_idle: for the top-level image:. A busy runner does not count, so a replacement starts as soon as an idle runner takes a job. Queued jobs get slots first, and floors fill the room left under your caps and Apple's limit of two macOS VMs. min_runners is unchanged. See Idle runners per pool and image.
Smaller container images
kinhin image prepare --container now bakes as layers on the base image instead of exporting a scratch container's whole filesystem. Disk use follows what kinhin adds, not the size of the base: a 31 GB base used to need about 250 GB free. The --container-image flag is checked like the config's base_image, and --clean-builder deletes the builder after a bake.
Fixes
- The menu bar app cleared every image's
warm_pool, and dropped a section'sfleet_labels:, when you edited a named image or a base image. Both are kept now. - A third Tart VM could start beside two warm ones when the Tart budget partition is 2, Apple's limit for macOS VMs. Warm VMs are now evicted to keep the partition.
fleet_labels: falseoncontainer:ordocker:was ignored by the running fleet and the Routing tab. They now agree withkinhin workflow route.min_runnersis one count for the whole fleet. The scaling page said it was per pool, and now says what it does.
Upgrading
There is no breaking change, and no config change is needed.
- The next
kinhin image prepare --containerbakes as layers. The baked image keeps the base'sENVandWORKDIR, runs asUSER rootand has noENTRYPOINT. The builder keeps a build cache (about 100 GB after a 31 GB base). Runcontainer builder stop && container builder deleteonce to reclaim an old bake's cache, or pass--clean-builder. - With
container.docker: true, pulls go through the Docker Hub cache unless you setdocker: { cache: false }.
What has been run
Run means it was executed for this release. Not yet means it has not been, and I am not claiming it.
- Run: the unit suite (no VMs or network) and the repository's release checks, locally on the release preparation commit.
- Not yet: a live fleet with named container pools, including
kinhin-container-NAMErouting and the app's pool editor. - Not yet: Docker inside a container runner, and the Docker Hub cache, on a live job.
- Not yet: an x86_64 runner under Rosetta, including a Docker build on it.
- Not yet:
min_idleagainst a real forge's runner list. - Not yet: ThreadSanitizer on the release commit.
- Not yet: an upgrade from beta 4 with real state, or the in-app update from beta 4.
- Not yet: the end-to-end suite on this release. It runs only in CI, and its dedicated Mac is not registered yet.
- Not yet: the Buildkite, Azure Pipelines and Bitbucket Pipelines lanes. They did not make beta 5 and move to beta 6. Azure stays experimental, Buildkite and Bitbucket stay planned, and GitHub, Gitea, Forgejo and GitLab CE remain the supported forges.
- Not yet: a long-running soak test.
Try it
The release is published on GitHub. Install it with:
curl -fsSL https://github.com/RabinApps/kinhin/releases/latest/download/install.sh | bashYou need an Apple Silicon Mac on macOS 15 or newer with about 60 GB of free disk (a baseline for running; image prepare --container on a large base image needs much more, and its builder cache can reach about 100 GB after a 31 GB base), and an access token for your forge. The Quick start goes from install to a running fleet. Coming from beta 4, the app can update itself, or you can install over it when no jobs are running.
Where I need help
- Container pools. Split your Linux jobs across two pools and tell me whether each job lands where you expect, and whether the app's pool editor does what it says.
- Docker in a container. Run a
docker buildor a job withservices:and tell me whether it worked, and whether the Docker Hub cache was used. - x86_64 runners. Run a job that needs
x64and tell me what breaks under Rosetta. min_idle. Set a floor and tell me whether the forge's runner list shows what you expect.- Lighter bakes. Re-bake a large base image and tell me how much disk it used.
- Real projects on the supported forges, and real Azure DevOps builds.
One real report is valuable, including when it fails. Open a forge or runtime report and attach the output of kinhin doctor --bundle after a glance through it, since it is redacted but not perfect.
Thank you
Thank you for installing the betas and reporting what you find, and to the contributors whose pull requests are in this release. kinhin is free and Apache-2.0. If it saves you CI minutes, you can support it.
Alex
