Documentation menu

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

FieldDefault
vm.name"klimax"
vm.cpus3/4 of your Mac's cores (10-core → 8)
vm.memoryhalf your Mac's RAM (32 GiB → "16GiB")
vm.disk"20GiB"
vm.rosettafalse (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.disablePortMirroringtrue
kind.nodeVersion"v1.36.1"
kind.metalLBVersion"v0.16.1"
kind.autoMergeKubeconfigtrue
kind.autoRemoveKubeconfigtrue
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
FieldMeaning
locationHost directory. ~ is expanded. Must exist — klimax up refuses a path that doesn't, rather than letting it fail deep inside the VM start.
writableDefaults to false, matching Lima. Opt in per directory.
mountPointOptional 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