README
ΒΆ
Fast, efficient, and opinionated multi-cluster manager for macOS Silicon laptops.
klimax is an dependency-free CLI that manages a macOS Virtualization.framework (VZ) Lima VM, installs Docker inside it, creates and manages multiple kind clusters, and wires up pure L3 routing from your Mac into the kind bridge subnet β no SNAT, no VPN, direct IP access to pods and LoadBalancer services.
Klimax is self-contained, clean, and can work alongside your current Docker setup without conflict (Orbstack, Colima, Rancher Desktop, etc.). More details below.

For lower-level design, see docs/KLIMAX-LLD-architecture.png.
Table of contents
- Demo
- Prerequisites
- Installation
- Quick start
- What it does
- Configuration reference
- CLI reference
- Networking deep-dive
- Running alongside Rancher Desktop, Colima, or kind-on-lima
- Project layout
Demo
Prerequisites
On your Mac (host)
- macOS 13 Ventura or later β Apple Virtualization.framework is required (
vmType: vz) sudoaccess β needed only whenklimax upfirst adds the macOS route (re-runs skip it if already correct) and forklimax destroy;klimax downdoes not require sudo- Go 1.22+ β only if building from source; not needed for the pre-built binary
- kubectx (optional) β for easier kubeconfig context switching, or use
klimax kubeconfig use <name>
klimax is self-contained. On first
klimax upit automatically downloads and caches the Lima guest agent binary. No separate Lima installation required.
Inside the VM (auto-provisioned by klimax up)
| Tool | Version | Purpose |
|---|---|---|
| Docker | latest via get.docker.com | Container runtime for kind and registries |
| kind | v0.32.0 | Kubernetes-in-Docker cluster manager |
| kubectl | latest stable | Cluster management from within the VM |
| jq, iptables, curl, net-tools, python3 | distro packages | Tooling for scripts and routing rules |
Installation
Homebrew (recommended)
brew tap bcollard/klimax
brew trust --tap bcollard/klimax
brew install --cask klimax
Upgrading
brew upgrade --cask klimax
β οΈ Rebuild the VM after upgrading. klimax bakes a specific kind CLI and default Kubernetes node image into the VM at creation time; upgrading the binary does not re-provision an existing VM. After a
brew upgrade, run:klimax destroy && klimax upthen recreate your clusters (
klimax cluster create <name>). Skipping this can leave you on an older kind that cannot create the new default node version.
Build from source
CGO is required because Lima's VM management packages link against macOS frameworks:
git clone https://github.com/bcollard/klimax
cd klimax
CGO_ENABLED=1 go build -o klimax ./cmd/klimax
sudo mv klimax /usr/local/bin/
Shell completion
# zsh (add to ~/.zshrc for persistence)
source <(klimax completion zsh)
# bash
source <(klimax completion bash)
# fish
klimax completion fish > ~/.config/fish/completions/klimax.fish
Quick start
# 1. Bring up the VM + Docker + networking + registries
klimax up # prompts for sudo once to add the macOS route; re-running up on a live VM skips it and won't prompt again
# 2. Create a kind cluster
klimax cluster create dev
# 3. Use the cluster (kubeconfig auto-merged into ~/.kube/config)
kubectl config use-context dev
# or: kubectx dev
# 4. Test cluster connectivity by deploying nginx and exposing it with a LoadBalancer service (MetalLB will assign a VIP in the kind bridge subnet)
klimax cluster e2e-test-nginx
# additionally, you can run a curl command from your Mac directly to the MetalLB VIP without port-forwarding:
# kubectl get svc nginx -o jsonpath='{.status.loadBalancer.ingress[0].ip}' # get VIP; by default in the 172.30.0.0/16 subnet
# curl http://<VIP>/ # should return the nginx welcome page
# 5. Create a second cluster
klimax cluster create staging
# 6. List all clusters
klimax cluster list
# 7. Delete one or more clusters
klimax cluster delete # with interactive picker (space to select)
# 8. Stop the VM when you're done (preserves clusters and registry cache)
klimax down
# 9. Destroy the VM and all clusters when you no longer need them
klimax destroy # this also removes the macOS route, so sudo is required
After klimax up, the kind bridge CIDR is routed from your Mac directly to the VM. You can reach any pod IP, Service ClusterIP, or MetalLB LoadBalancer IP without port-forwarding.
What it does
| Concern | What klimax does |
|---|---|
| VM | Creates/starts/stops/deletes a Lima VZ instance |
| Docker | Installs Docker in the VM; forwards the socket to ~/.<vmname>.docker.sock |
| kind | Creates/deletes multiple kind clusters; each gets its own subnet slice and API port |
| Registries | Runs pull-through mirrors for docker.io, quay.io, gcr.io, and Google Artifact Registry (us-docker.pkg.dev, us-central1-docker.pkg.dev); mirror data cached persistently |
| Networking | Routes kindBridgeCIDR from macOS β VM via lima0; no SNAT so source IPs are preserved |
| MetalLB | Installed in every cluster with a dedicated IP pool slice |
| CoreDNS | Adds custom domain forwarding (e.g. runlocal.dev) at cluster creation |
| kubeconfig | Exports per-cluster kubeconfig to ~/.kube/klimax/<name>.kubeconfig; auto-merges into ~/.kube/config |
Configuration reference
The default config path is ~/.klimax/config.yaml. Use klimax config edit to open it in your $EDITOR, or copy config.example.yaml to get started.
# ββ VM ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
vm:
name: "klimax" # Lima instance name; Docker socket at ~/.<name>.docker.sock
cpus: 4
memory: "10GiB"
disk: "40GiB" # grow later with: klimax disk resize 80GiB
# rosetta: false # enable Rosetta 2 for amd64 containers (ARM64 only)
# imageDisk: "10GiB" # persistent image store disk; survives `klimax destroy`
# ββ Networking βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
network:
kindBridgeCIDR: "172.30.0.0/16" # routed from macOS β VM; no SNAT
# Defaults to true: Lima's TCP port mirroring is disabled, so klimax coexists with
# other Lima VMs (kind-on-lima, Rancher Desktop) that also manage kind clusters β
# otherwise two VMs racing to mirror the same port (e.g. 7001) to 127.0.0.1 conflict.
# With disablePortMirroring: true, kubeconfigs use the VM's direct lima0 IP instead
# of 127.0.0.1. Set to false to force loopback (127.0.0.1) β e.g. host security
# software (CrowdStrike) blocking vzNAT IPs. β VM-level: only takes effect on new
# VMs (klimax destroy && up).
# disablePortMirroring: true
# ββ Kind defaults (applied to every `klimax cluster create`) βββββββββββββββββ
kind:
nodeVersion: "v1.36.1"
metalLBVersion: "v0.16.1"
customDnsResolvers:
- domain: "runlocal.dev" # forward to 8.8.8.8/8.8.4.4 (default resolvers)
# - domain: "corp.internal"
# resolvers: ["10.0.0.53"] # private resolver for internal zones
autoMergeKubeconfig: true # merge context into ~/.kube/config after create
autoRemoveKubeconfig: true # remove context from ~/.kube/config after delete
# ββ Registries βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
registries:
# "host" (default): cache at ~/.klimax/registry-cache/ β survives klimax destroy
# "guest": cache inside the VM β wiped on klimax destroy
cacheStorage: "host"
mirrors:
- name: "registry-dockerio"
port: 5030
remoteURL: "https://registry-1.docker.io"
# username/password: optional, avoids Docker Hub rate limits
- name: "registry-quayio"
port: 5010
remoteURL: "https://quay.io"
- name: "registry-gcrio"
port: 5020
remoteURL: "https://gcr.io"
- name: "registry-us-docker-pkgdev"
port: 5040
remoteURL: "https://us-docker.pkg.dev"
- name: "registry-us-central1-docker-pkgdev"
port: 5050
remoteURL: "https://us-central1-docker.pkg.dev"
Cluster lifecycle is managed exclusively via
klimax clustersubcommands β there is no cluster list in the config file.
CLI reference
klimax [--config ~/.klimax/config.yaml] [--debug] [--lima-log-level <level>] <command>
By default only klimax's own logs are shown; the underlying Lima VM logs are
hidden. Use --lima-log-level trace|debug|info|warn|error|off to surface them
(or --debug, which shows Lima at info).
VM lifecycle
| Command | Description |
|---|---|
klimax up |
Create/start the VM, provision Docker, set up networking and registries (idempotent) |
klimax down |
Stop the VM β preserves all clusters and registry cache data |
klimax down --remove-route |
Stop the VM and remove the macOS host route (requires sudo) |
klimax destroy |
Stop + delete VM, delete all clusters, remove host route |
klimax status |
Show VM state, clusters, route, and iptables rule presence |
klimax doctor |
Diagnose common issues (VM, route, iptables, IP forwarding, Rosetta) |
klimax version |
Print the klimax version |
klimax shell |
Open an interactive SSH session in the VM |
klimax shell <cmd> [args...] |
Run a command in the VM and exit with its exit code |
klimax copy <src>... <dst> |
Copy files between host and VM (vm: marks the VM side; -r for directories) |
klimax config edit |
Open the config file in $VISUAL / $EDITOR |
klimax disk resize <size> |
Grow the VM disk (e.g. 80GiB) β applied on the next VM start |
klimax prune |
Remove reclaimable cached files (--dry-run, --downloads, -y) |
klimax sudoers |
Print a sudoers snippet so klimax up never prompts for the host route (--check) |
klimax autostart install|uninstall|status |
Manage a launchd agent that starts the VM at login |
Running commands and copying files
klimax shell doubles as a non-interactive runner, so VM-side work composes in
scripts and pipelines without needing limactl:
klimax shell docker ps # flags after the command are passed through
klimax shell -- bash -c 'kind get clusters' # use -- when flags could look like klimax's
cat setup.sh | klimax shell bash -s # stdin is piped through
klimax shell -t htop # -t forces a pseudo-terminal
The exit code is the remote command's, so if klimax shell test -e /run/docker.sock; then ... works.
klimax copy ./script.sh vm:/tmp/script.sh # host β VM
klimax copy vm:/tmp/out.json ./out.json # VM β host
klimax copy -r ./manifests vm:/tmp/manifests # directories
Growing the disk
klimax disk resize 80GiB # updates vm.disk and the Lima instance config
klimax down && klimax up # Lima expands the image; the guest FS grows on boot
Shrinking is not supported. Node images are large (~2.7GB each), so check
headroom with klimax shell df -h /.
Reclaiming space
klimax prune --dry-run # show what would go
klimax prune --downloads # also clear Lima's shared image download cache
It removes superseded Lima guest agents and registry cache directories whose
mirror is no longer configured. Caches of configured mirrors are never touched
β use klimax registry clean-cache for those.
Passwordless host route (and starting at login)
klimax up needs root for exactly one thing: the macOS route for the kind bridge
CIDR. Grant just that, and up stops prompting:
klimax sudoers | sudo tee /etc/sudoers.d/klimax >/dev/null
sudo chmod 0440 /etc/sudoers.d/klimax
klimax sudoers --check # inspects the sudo policy for the two rules
That is also what makes autostart useful, since launchd cannot answer a prompt:
klimax autostart install # runs 'klimax up' at login; --print shows the plist
klimax autostart status
klimax autostart uninstall
Autostart output is appended to ~/.klimax/logs/autostart.log.
Docker
Option A β environment variable (current shell only)
eval $(klimax docker-env) # export DOCKER_HOST=unix://...
eval $(klimax docker-env --unset) # unset DOCKER_HOST
Option B β Docker context (persistent across shells)
klimax docker-context # create/update "klimax" context
klimax docker-context --unset # docker context use default
If
DOCKER_HOSTis set it overrides the active Docker context β use one or the other.klimax docker-contextwarns when both are active.
Clusters
# Create
klimax cluster create <name>
klimax cluster create <name> --region us-east1 --zone us-east1-a
klimax cluster create <name> -l team=platform -l env=dev # extra node labels
# Apply a fleet from a Fleet manifest (see below)
klimax cluster apply -f fleet.yaml
klimax cluster apply -f fleet.yaml --dry-run
klimax cluster apply -f fleet.yaml --max-parallel 3
# Delete β interactive multi-select picker when no name given
klimax cluster delete <name>
klimax cluster delete
klimax cluster delete -f fleet.yaml --yes # tear down a whole fleet
klimax cluster delete -l env=test --yes # delete by label selector
# List
klimax cluster list
klimax cluster list -o json
klimax cluster list -l klimax.dev/fleet=dev-fleet # filter by label selector
# Label an existing cluster's nodes
klimax cluster label <name> -l team=platform -l env=prod # set/overwrite
klimax cluster label <name> -l env- # remove
# Kubeconfig
klimax kubeconfig use <name> # merge + switch active kubectl context to it
klimax kubeconfig merge <name> # merge into ~/.kube/config (don't switch)
eval $(klimax kubeconfig env <name>) # or point KUBECONFIG at the isolated file
klimax kubeconfig path <name> # print the kubeconfig file path
klimax kubeconfig remove <name> # remove the context from ~/.kube/config
# E2E smoke test (uses current kubectl context)
klimax cluster e2e-test-nginx
klimax cluster e2e-test-nginx --cleanup # remove nginx pod/svc only
The interactive delete picker supports multi-select:
Delete kind clusters (β/β navigate Β· Space toggle Β· a=all Β· Enter confirm Β· q quit)
[ ] dev port 7001
[x] staging port 7002
[x] prod port 7003
2 cluster(s) selected β press Enter to delete
Fleets β klimax fleet
Create and manage several clusters at once from a declarative Fleet manifest. The minimal manifest lists only names β everything else defaults:
# fleet.yaml
apiVersion: klimax.dev/v1alpha1
kind: Fleet
metadata:
name: dev-fleet
spec:
clusters:
- dev
- staging
klimax fleet create -f fleet.yaml # create the clusters (--dry-run to preview, --max-parallel N)
klimax fleet list # show fleets and their member clusters
klimax fleet describe dev-fleet # members with num, ports, node version/readiness, labels
klimax fleet label dev-fleet -l tier=gold # label every cluster in the fleet (key- to remove)
klimax fleet delete dev-fleet --yes # delete every cluster in the fleet
klimax fleet delete -f fleet.yaml --yes # ...or tear down exactly what the manifest lists
klimax fleet adopt dev-fleet legacy1 # pull an existing standalone cluster into the fleet
If fleet create finds a listed cluster that already exists but isn't part of the
fleet, it warns and skips it (never silently relabels). Re-run with --adopt
to pull those clusters into the fleet, or use klimax fleet adopt <fleet> <cluster>β¦.
Fleet membership is tracked by the klimax.dev/fleet=<name> node label, so
fleet list/label/delete work on the live clusters regardless of the manifest.
create is additive: clusters that already exist are skipped. Each entry can
optionally set dependsOn (ordering), num, nodeVersion, region/zone,
registries (cherry-pick mirrors),
addons.metricsServer, and labels. Independent clusters build in parallel up to
spec.maxParallel; dependsOn is always respected. See
examples/fleet.yaml for the full reference.
klimax cluster apply -f/cluster delete -fremain as lower-level equivalents offleet create/fleet delete -f.
Per-cluster resources (derived from the auto-assigned cluster num)
| Resource | Value |
|---|---|
| API server host port | 700N (e.g. 7001 for num 1) |
| Service subnet | 10.N.0.0/16 |
| Pod subnet | 10.1N.0.0/16 |
| MetalLB pool | <kindPrefix>.N.1β7 and <kindPrefix>.N.16β254 |
| kubeconfig | ~/.kube/klimax/<name>.kubeconfig |
| topology labels | topology.kubernetes.io/region=europe-westN, zone=europe-westN-b |
| default node labels | managed-by=klimax (always); klimax.dev/fleet=<name> (via apply -f); plus any -l key=value / Fleet labels |
Registries
klimax registry clean-cache # remove all mirror cache dirs + containers; run 'klimax up' to restart
Mirror cache data is stored at ~/.klimax/registry-cache/<mirror-name>/ by default (cacheStorage: "host"), virtiofs-mounted into the VM and bind-mounted into each registry container. Blobs survive klimax down/up cycles and even klimax destroy.
AI coding tools (Agent Skill)
klimax ships an Agent Skill that teaches AI coding tools how to drive klimax β spinning up ephemeral kind clusters for scripts, demos, and e2e tests. Install it once and every future agent session knows how to use klimax without you explaining it each time:
klimax skill install # β ~/.claude/skills/klimax/SKILL.md
klimax skill install --force # overwrite an existing copy (e.g. after upgrading klimax)
klimax skill path # print the install path
klimax skill install --print # emit the skill to stdout (pipe it anywhere)
The skill is embedded in the binary, so no download is needed. Start a new agent session after installing to pick it up.
Networking deep-dive
macOS host
bridge1xx (<host-IP>, macOS-assigned)
β vzNAT β Apple VZNATNetworkDeviceAttachment
βΌ
Lima VZ guest
lima0 (<guest-IP>, macOS-assigned)
br-<id> (172.30.0.1/16) β Docker bridge "kind"
β
kind cluster nodes (172.30.N.x)
vzNAT IPs are assigned by macOS and cannot be configured. klimax detects the VM's
lima0IP at runtime β nothing is hardcoded. Multiple Lima VMs (klimax, limactl, colimaβ¦) each get a distinct IP on their ownbridge1xx, so they coexist without conflict.
How pure L3 routing works
klimax upadds a macOS route:172.30.0.0/16 β <lima0-IP>(viasudo /sbin/route).- Inside the VM,
ip_forward=1and a systemd oneshot apply iptables rules idempotently:- nat exemption (before Docker's MASQUERADE): kindβhost traffic exits
lima0without SNAT. - DOCKER-USER forward rules: hostβkind and established returns are explicitly allowed.
- nat exemption (before Docker's MASQUERADE): kindβhost traffic exits
- A
docker.service.ddrop-in reruns the rules after every Docker restart.
The result: curl http://172.30.1.200/ on your Mac reaches the MetalLB VIP directly.
kubeconfig and API server access
klimax supports two modes for kubeconfig API server addresses:
Default (direct IP mode β network.disablePortMirroring: true)
Lima's TCP port mirroring is disabled for the klimax VM, so klimax coexists cleanly with other Lima-based VMs (kind-on-lima, Rancher Desktop) that also manage kind clusters β otherwise two VMs racing to mirror the same port (e.g. 7001) to 127.0.0.1 conflict. Kubeconfigs use the VM's direct lima0 IP (e.g. 192.168.64.3:700N), which is L2-reachable from the host via vzNAT. The API server cert automatically includes the lima0 IP as a SAN.
Note: the lima0 IP is assigned dynamically by macOS and may change on VM restart. Run
klimax kubeconfig merge <name>after a restart to refresh the address in~/.kube/config.
Loopback mode (network.disablePortMirroring: false)
Cluster API servers listen on 0.0.0.0:700N inside the VM. Lima's hostagent forwards these ports to 127.0.0.1:700N on the host, and exported kubeconfigs point at https://127.0.0.1:700N β a stable address that survives VM restarts. Use this when running only a single klimax VM, or when host-based security software (e.g. endpoint agents like CrowdStrike) blocks direct vzNAT IP access.
This is a VM-level setting β it only takes effect on new VMs (klimax destroy && klimax up).
Registry mirrors
Every kind cluster is configured via containerd patches to cache pulls from docker.io, quay.io, gcr.io, and Google Artifact Registry (us-docker.pkg.dev, us-central1-docker.pkg.dev) transparently through pull-through mirrors, avoiding rate limits and accelerating cluster creation.
All mirror containers are attached to the kind Docker network so cluster nodes resolve them by hostname.
Running alongside Orbstack, Rancher Desktop, or Colima
klimax is designed to coexist with other Lima-based tools on the same Mac. Each Lima VM gets
its own vzNAT interface (bridge1xx) and a distinct macOS-assigned IP, so there is no
IP-level conflict between VMs.
The one friction point would be Lima's TCP port mirroring: Lima's hostagent can
forward every TCP port that a process in the VM listens on to 127.0.0.1 on the host. When
two VMs independently manage kind clusters, both hostagents would try to mirror the same
API-server ports (e.g. 7001) to 127.0.0.1 simultaneously, breaking connectivity for both.
klimax bypasses this by default β network.disablePortMirroring is true:
# ~/.klimax/config.yaml
network:
disablePortMirroring: true # default
With this setting (the default):
- Lima does not forward any TCP port from the klimax VM to
127.0.0.1. - Cluster kubeconfigs use the VM's direct
lima0IP (e.g.https://192.168.64.3:7001) β reachable from the host over vzNAT without any routing or VPN. - The API server cert automatically includes the
lima0IP as a SAN, so TLS verification works out of the box. - Every other Lima VM (Rancher Desktop, Colima, kind-on-lima) keeps forwarding its own ports to
127.0.0.1completely unaffected.
This is a VM-level setting β it is applied when the VM is created. (Set it to false before
klimax up if you need stable 127.0.0.1 kubeconfigs or your host security software blocks
vzNAT IPs.)
The
lima0IP is assigned dynamically by macOS and may change on VM restart. Runklimax kubeconfig merge <name>after a restart to refresh kubeconfigs.
See docs/klimax-vs-other-lima-based-tools.md for a detailed comparison with Rancher Desktop, Colima, and kind-on-lima.
Project layout
klimax/
βββ cmd/klimax/ # main entrypoint
βββ internal/
β βββ cli/ # Cobra commands
β βββ config/ # YAML schema, defaults, validation
β βββ vm/ # Lima instance manager + guest agent download
β βββ limatemplate/ # Builds limatype.LimaYAML (VZ, mounts, provision script)
β βββ guest/ # SSH client for running commands/scripts in the VM
β βββ docker/ # Docker network management in guest
β βββ registry/ # Pull-through mirror lifecycle
β βββ kind/ # kind cluster create/delete/list; MetalLB, CoreDNS, kubeconfig
β βββ routing/ # iptables no-NAT rules; macOS route management
βββ config.example.yaml
βββ .goreleaser.yaml
βββ Makefile
License
MIT
Documentation
ΒΆ
Overview ΒΆ
Package klimax exposes repo-root assets that are compiled into the binary.
Index ΒΆ
Constants ΒΆ
This section is empty.
Variables ΒΆ
var SkillMD string
SkillMD is the canonical klimax Agent Skill definition, embedded from the repo-root SKILL.md. `klimax skill install` ships it inside the binary so the installed CLI can drop the skill into an AI coding tool's skills directory without a separate download. SKILL.md remains the single source of truth.
Functions ΒΆ
This section is empty.
Types ΒΆ
This section is empty.
Directories
ΒΆ
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
klimax
command
|
|
|
internal
|
|
|
fleet
Package fleet parses and validates the klimax Fleet manifest β a declarative description of a fleet of kind clusters applied via `klimax cluster apply -f`.
|
Package fleet parses and validates the klimax Fleet manifest β a declarative description of a fleet of kind clusters applied via `klimax cluster apply -f`. |
|
registry
Package registry manages the pull-through mirror containers running inside the klimax Lima VM.
|
Package registry manages the pull-through mirror containers running inside the klimax Lima VM. |