Add support for apple-container builder engine - #1935
Conversation
Building with Kamal requires Docker on the developer's machine, which on macOS means Docker Desktop. Apple's `container` CLI builds and pushes OCI images natively on Apple silicon, but Kamal hardcodes `docker` for every local build, push and registry command, so there is no way to use it. Add `builder/engine`, defaulting to `docker`. Setting it to `apple-container` routes the commands that run on this machine through `container` instead. The deployment servers are untouched and still use Docker. The engine is chosen by picking an object rather than by branching inside the command classes. Kamal::Commands::Builder#local is the only place the engine name is read, and it returns Builder::AppleContainer in place of Builder::Local. Everything downstream is polymorphic on that object: the builder answers #local_registry with Registry::AppleContainer in place of Registry, and #install_error with the missing-dependency message for its engine. Anything that runs on the hosts goes through KAMAL.registry, which stays Docker-only, so Builder#target, Registry#login and Registry#logout are unchanged. - `container build` has no registry exporter, so a push is a build followed by one `container image push` per tag. - `registry/scheme` (auto, http, https) is read only by the apple paths, and falls back to http for a localhost registry. `--scheme auto` cannot reach one: it attempts TLS, hangs, and fails with "bad protocol version". - `builder/ssh` is resolved and exported as SSH_AUTH_SOCK, because `container build` accepts only the literal `--ssh default` and reads the socket from its own environment. - `kamal build remove` stops the builder rather than deleting it. `container` has a single machine-wide builder, so deleting it would discard every project's build cache. - Kamal::Docker.included_files takes the builder and asks it for the check image's build and run argv, and checks `status.success?` rather than the always-truthy Process::Status it was testing before. - Unsupported configuration is rejected up front: remote builders, disabled local builds, buildpacks, cache exports, provenance and SBOM attestations, custom builder drivers, and non-default SSH agents. `kamal build dev` now passes push_env to the build command, so with a remote builder it exports BUILDKIT_NO_CLIENT_TOKEN the way `kamal build push` already did. This affects the Docker path too. Verified by hand against `container` 1.2.2 on macOS 26: building and pushing both tags to an authenticated local registry, a multi-arch manifest list from repeated --platform, and SSH agent forwarding. The test suite was compared against main by failing-test set rather than by count, and the sets are identical both on Linux with Ruby 4.0 and on macOS with Docker running. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds Apple’s container CLI as an optional local builder engine while retaining Docker on deployment servers.
Changes:
- Routes local build, push, and registry operations through the configured engine.
- Adds engine-specific validation, documentation, and tests.
- Fixes Docker included-file status handling and dev-build environment forwarding.
Tip
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
test/fixtures/deploy_with_apple_container.yml |
Adds an Apple container fixture. |
test/docker_test.rb |
Tests included-file checks and failures. |
test/configuration/validation_test.rb |
Tests engine and scheme validation. |
test/configuration/builder_test.rb |
Tests engine configuration. |
test/commands/registry_test.rb |
Tests Apple registry commands. |
test/commands/builder_test.rb |
Tests Apple builder commands. |
test/cli/registry_test.rb |
Tests mixed local/remote registry behavior. |
test/cli/build_test.rb |
Tests Apple push and dev flows. |
lib/kamal/docker.rb |
Makes included-file checks engine-aware. |
lib/kamal/configuration/validator/registry.rb |
Validates registry schemes. |
lib/kamal/configuration/validator/builder.rb |
Validates engine capabilities. |
lib/kamal/configuration/registry.rb |
Resolves registry schemes. |
lib/kamal/configuration/docs/registry.yml |
Documents registry schemes. |
lib/kamal/configuration/docs/builder.yml |
Documents builder engines. |
lib/kamal/configuration/builder.rb |
Exposes engine configuration. |
lib/kamal/commands/registry/apple_container.rb |
Implements Apple registry commands. |
lib/kamal/commands/registry.rb |
Centralizes the registry container name. |
lib/kamal/commands/builder/base.rb |
Adds shared engine interfaces. |
lib/kamal/commands/builder/apple_container.rb |
Implements the Apple builder. |
lib/kamal/commands/builder.rb |
Selects the configured local engine. |
lib/kamal/commands/base.rb |
Adds Apple CLI command construction. |
lib/kamal/commander.rb |
Exposes engine-specific local registry commands. |
lib/kamal/cli/registry.rb |
Routes local registry operations by engine. |
lib/kamal/cli/build.rb |
Routes local builds and environments by engine. |
lib/kamal/cli/base.rb |
Generalizes dependency checking. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| def push_env | ||
| if (socket = ssh_socket) | ||
| { "SSH_AUTH_SOCK" => socket } | ||
| else | ||
| {} | ||
| end | ||
| end |
There was a problem hiding this comment.
Good catch, fixed in f33e335.
The suggested patch raises NameError though — Ruby parses socket in the body as a
method call, since the assignment is lexically later on the line. Assigning first works:
socket = ssh_socket
{ "CONTAINER_DEFAULT_PLATFORM" => "" }.tap do |env|
env["SSH_AUTH_SOCK"] = socket if socket
end
| # `auto` is the container CLI's own default, and it cannot reach a local | ||
| # registry: it attempts TLS and fails. Treat it as "let Kamal decide". |
There was a problem hiding this comment.
Not reproducible on container 1.2.2 / macOS 26.6.2. Against a plain-HTTP registry:3 on 127.0.0.1:5099:
container image push localhost:5099/probe:1 -> Error: -9836: bad protocol version
container image push 127.0.0.1:5099/probe:1 -> Error: -9836: bad protocol version
container image push --scheme http localhost:5099/probe:1 -> 100% (4 blobs, 1,8 MB) [0s]
Only the explicit-HTTP push landed. Keeping the behaviour; the comment now names the error, in a1e5c30.
The push commands carry no --platform: they push whatever tags the build produced, so a multi-arch build sends a manifest list. `container` fills a missing --platform from CONTAINER_DEFAULT_PLATFORM, so a developer with that variable exported would push one architecture under a tag Kamal built for two, and the servers on the other architecture would fail to pull it. Export the variable empty for the build-and-push command, which 1.2.2 reads as unset. The build itself is unaffected either way, because the --platform flags it passes take precedence over the variable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Naming the failure makes it clear why Kamal picks http for a localhost registry rather than leaving the scheme to `container`: 1.2.2 attempts TLS for localhost and 127.0.0.1 alike, and fails with "bad protocol version". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds a builder/engine, defaulting to docker. Setting it to apple-container routes the local build, push and registry commands through Apple's container CLI instead. The deployment servers are untouched and still use Docker.
Two fixes land on the Docker path too: Kamal::Docker.included_files checks status.success? rather than the always-truthy Process::Status it was testing before, and kamal build dev passes push_env the way kamal build push already did.
Verified by hand against Apple's container CLI 1.2.2 on macOS 26. CI covers the Docker paths.