Configuration
klimax reads a single YAML file. Defaults are sensible enough that most people never touch it — but every knob, from VM size to the bridge CIDR to registry mirrors, lives here.
The config file
The default path is ~/.klimax/config.yaml. Open it in your
$EDITOR with:
$ klimax config edit
Or point any command at a specific file:
$ klimax up -c config.yaml
The quickest way to start is from the shipped
config.example.yaml — it documents every field inline.
Full reference
The full annotated example, with every field and its default:
# ── VM ──
vm:
name: "klimax" # Lima instance name; Docker socket at ~/.<name>.docker.sock
# cpus and memory default to a share of this Mac — omit them and klimax sizes
# the VM for the machine it is on. Set them to pin a value instead.
cpus: 8 # 3/4 of the cores (10-core -> 8)
memory: "16GiB" # 16GiB -> 8GiB, 32GiB -> 16GiB, 64GiB -> 32GiB
disk: "20GiB" # default; images live on imageDisk, not here. Grow: klimax disk resize 40GiB
# rosetta: false # transparent amd64 containers (Apple Silicon); needs softwareupdate --install-rosetta
imageDisk: "30GiB" # default; separate Lima disk for the image store, survives klimax destroy (VM-level)
mounts: [] # host dirs shared into the VM (virtiofs); empty by default
# mounts:
# - location: "~/projects" # "~" expanded; must exist
# writable: true # default false
# - location: "~/data"
# mountPoint: "/srv/data" # optional; a differing guest path breaks -v <host path>
# ── Networking ──
network:
kindBridgeCIDR: "172.30.0.0/16" # routed macOS → VM lima0, no SNAT (default)
# disablePortMirroring: true # default true → kubeconfigs use the lima0 IP; set false to force 127.0.0.1; VM-level (needs destroy && up)
# ── Kind (applied to every cluster create) ──
kind:
nodeVersion: "v1.36.1" # kindest/node tag — matched to the bundled kind CLI; overriding warns
metalLBVersion: "v0.16.1" # MetalLB version (default)
customDnsResolvers: # CoreDNS forwarding rules
- domain: "runlocal.dev"
# resolvers: ["8.8.8.8", "8.8.4.4"] # default when omitted
# - domain: "corp.internal"
# resolvers: ["10.0.0.53"]
# autoMergeKubeconfig: true # merge new context into ~/.kube/config (default: true)
# autoRemoveKubeconfig: true # remove context on deletion (default: true)
# ── Registries ──
registries:
# cacheStorage: "host" # "host" (default) survives klimax destroy; "guest" wiped on destroy
mirrors: # name must NOT be a hostname (no '.' or ':') — it would shadow the upstream
- name: "registry-dockerio"
port: 5030
remoteURL: "https://registry-1.docker.io"
# username / password # optional, to avoid 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"
Defaults
| Field | Default |
|---|---|
vm.name | "klimax" |
vm.cpus | 3/4 of your Mac's cores (10-core → 8) |
vm.memory | half your Mac's RAM (32 GiB → "16GiB") |
vm.disk | "20GiB" |
vm.rosetta | false (opt-in; Apple Silicon only) |
vm.imageDisk | "30GiB" |
vm.mounts | [] (nothing shared; writable defaults to false per entry) |
network.kindBridgeCIDR | "172.30.0.0/16" |
network.disablePortMirroring | true |
kind.nodeVersion | "v1.36.1" |
kind.metalLBVersion | "v0.16.1" |
kind.autoMergeKubeconfig | true |
kind.autoRemoveKubeconfig | true |
registries.cacheStorage | "host" |
| DNS resolvers (when omitted) | ["8.8.8.8", "8.8.4.4"] |
Before creating the VM, klimax up reviews your config: it
lists any options this klimax version supports that your file doesn't set
(defaults apply — useful when a config predates an upgrade), and if
kind.nodeVersion is pinned away from the version matched to the
bundled kind CLI it offers to update it.
Cluster lifecycle is managed only through klimax cluster
subcommands — there is no list of clusters in the config file.
Per-cluster subnets, ports and MetalLB pools are derived automatically
from each cluster's slot number.
Most settings above (vm.*, the bridge CIDR,
network.disablePortMirroring) are VM-level —
they only take effect on a fresh VM. Apply a change by re-creating it:
klimax destroy && klimax up. The same is true after a
klimax upgrade: destroy and re-create the VM so the new
binary re-provisions it.
vm.disk is the exception — it can be
grown in place, no destroy needed.
klimax disk
resize 80GiB rewrites this field and the Lima instance
config; Lima expands the image and the guest filesystem grows on the next
boot, so a klimax down && klimax up applies it.
Shrinking is refused.
vm.mounts is the other exception — see
Sharing host directories. klimax up
applies a change by restarting the VM, no destroy needed.
Sharing host directories
The Docker daemon runs inside the VM, so it resolves a bind-mount source inside the VM. klimax shares nothing of yours by default — only its own registry cache — and a bind for an unshared path does not fail:
# Without a matching vm.mounts entry, /etc/app is empty in the container.
docker run -v ~/projects/conf:/etc/app alpine ls /etc/app
The daemon simply creates the missing directory in the guest and the
container sees an empty one. List the directories you
want shared and they appear in the VM at the same absolute
path, which is exactly what makes a host-path
-v work:
vm:
mounts:
- location: "~/projects"
writable: true
klimax up # spots the change and offers to restart the VM
klimax status # shows the VM's current shares
| Field | Meaning |
|---|---|
location | Host directory. ~ is expanded. Must exist — klimax up refuses a path that doesn't, rather than letting it fail deep inside the VM start. |
writable | Defaults to false, matching Lima. Opt in per directory. |
mountPoint | Optional guest path. Leave it out unless you mean it: a guest path that differs from the host path is the one case where -v <host path> stops working. |
Unlike vm.imageDisk and
network.disablePortMirroring, this list does not need the VM
re-created. klimax up compares it against the VM's current
shares and offers to restart — a restart still stops every kind cluster
on the VM, so it asks first, and changes nothing when run
non-interactively.
Mounts are host-directory shares (virtiofs) only. The VM's root disk
(vm.disk) and the image store
(vm.imageDisk) are block devices, not mounts, and are
unaffected by anything here.
Persistent image store
klimax attaches a separate, named Lima data disk and mounts it over
/var/lib/containerd in the guest, so
klimax destroy no longer takes the image store with the VM.
This is on by default at 30GiB; set it to "" to
put the image store back on the root disk (and raise
vm.disk to match):
vm:
imageDisk: "30GiB" # default
The disk lives in ~/.klimax/_disks/<vm>-img and outlives
the VM, so kindest/node, registry:2 and any
image you built locally are still there after a
destroy && up. That last one is the case that
matters: a locally built image is the only kind no registry mirror can
restore.
10 GiB is a comfortable size — the store sits around 1–3 GiB in practice, even with several node images cached. Leave the field unset to keep the old behaviour.
This saves roughly a minute per VM re-create, not per
cluster — klimax cluster create and delete
already reuse the VM's image store. A warm mirror pull is ~2 s; the
unpack it can't avoid is ~15 s per node image. Like the other
vm.* fields, it only takes effect on a new VM.
Behind a corporate proxy
klimax already picks up your Mac's system proxy settings — you do not
need to configure anything for the common case. Set
network.proxy only to point at a different proxy, or one
macOS does not know about:
network:
proxy:
http: "http://proxy.corp:3128"
https: "http://proxy.corp:3128"
noProxy: ["*.corp.example"] # appended to the list klimax computes
inheritFromHost: true # default; use macOS settings when http/https are unset
The proxy is applied to everything that pulls: the Docker daemon, the registry mirrors, and the containerd inside every kind node.
klimax computes no_proxy for you, and
that is the part worth not doing by hand: the kind bridge CIDR, the
cluster service and pod subnets, the host↔VM subnet, and every
registry mirror name. A corporate proxy has no route to any of them, so
sending cluster-internal traffic there turns every in-cluster call into a
timeout that looks like a klimax fault. Your noProxy entries
are added to that list, not instead of it.
Applied by klimax up without recreating the VM. Removing
network.proxy removes it again.
Trusting extra certificate authorities
A proxy that terminates TLS — corporate interception — presents
its own certificate, so pulls fail with
x509: certificate signed by unknown authority even when the
proxy itself is configured correctly. The same applies to a private
registry with a self-signed chain. Point klimax at the PEM:
vm:
caCerts:
files: ["~/corp-root-ca.pem"]
The certificate is installed in three places, because each has its own trust store: the VM itself, every registry mirror, and every kind node. Nodes are configured at creation, so clusters made before you added a certificate need recreating to pick it up.
klimax checks the file is really a certificate before installing it,
and refuses one containing a private key. A DER file needs converting
first: openssl x509 -inform der -in cert.der -out cert.pem.
See also
- Networking & L3 routing — how the bridge CIDR becomes routable.
- Reset & reinstall — what survives a
destroy, and how to wipe it all. - Registry mirrors — the pull-through caches and how to add your own.
- CLI reference — every command and flag.