03. Project files and flake entry point
You will not create a new project yet. The goal is to recognize the files Kix looks for and what each one contributes. The next tutorial can then start from an empty directory without making the Nix pieces feel like magic.
Prerequisites
Section titled “Prerequisites”- Deploy
02-hello-worldfromkix-examplesin Local setup and first run.
The Basic Files
Section titled “The Basic Files”A small Kix repo usually starts like this:
- flake.nix
- cluster.nix
Directorypackages/
- my-package.nix
The first example repo is slightly more structured because it contains many numbered examples:
- flake.nix
- kind-config.yaml
Directorytutorials/
Directory02-hello-world/
- README.md
- cluster.nix
Both layouts use the same Kix idea:
flake.nixis the project entry point;- each
cluster.nixfile says what one cluster contains; - package files under
packages/hold reusable package definitions.
The Flake Is The Entry Point
Section titled “The Flake Is The Entry Point”A flake.nix file is a Nix project definition. In a Kix repo, it answers two
basic questions:
- What does this project depend on?
- Which named clusters does this project expose?
The Kix CLI reads that file so commands like kix list clusters and
kix deploy 02-hello-world know what clusters exist.
You do not need to understand every Nix construct yet. Read this example for
three landmarks: inputs, mkFlake, and clusters.
{ description = "Kix example clusters — a tour of Kix patterns runnable on kind.";
inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
# Fetched via the GitHub fetcher. While the repo is private, every # consumer (local dev, kixpkgs CI, the captures pipeline) must # provide a token: `gh auth token` plugged into nix.conf # `access-tokens = github.com=...`, or the workflow's auto-issued # GITHUB_TOKEN. The kixpkgs captures pipeline rewrites this URL # to `path:<store>` and drops flake.lock before invoking kix, so # its sandboxed eval never hits the network. (kix's clap CLI # doesn't forward `--override-input`, so the rewrite is the only # in-band knob.) kixpkgs.url = "github:adamcharnock/kixpkgs"; kixpkgs.inputs.nixpkgs.follows = "nixpkgs"; };
# Each example folder has its own cluster.nix. Add a new example by # appending a line here and creating examples/NN-name/cluster.nix. outputs = inputs: inputs.kixpkgs.lib.mkFlake { inherit inputs; clusters = { "02-hello-world" = ./tutorials/02-hello-world/cluster.nix; "06-service-dep" = ./tutorials/06-service-dep/cluster.nix; "08-namespace-deps" = ./tutorials/08-namespace-deps/cluster.nix; "09-typed-options" = ./tutorials/09-typed-options/cluster.nix; "10-reuse-packages" = ./tutorials/10-reuse-packages/cluster.nix; "19-scorecards" = ./tutorials/19-scorecards/cluster.nix; "how-to-application" = ./how-to/application/cluster.nix; "how-to-adoption" = ./how-to/adoption/cluster.nix; "how-to-stateful-migration" = ./how-to/adoption/stateful-before-cluster.nix; "how-to-adoption-takeover" = ./how-to/adoption/takeover-cluster.nix; "how-to-helm-bridge" = ./how-to/adoption/helm-bridge-cluster.nix; "how-to-auto-instantiation" = ./how-to/auto-instantiation/cluster.nix; "how-to-composition" = ./how-to/composition/cluster.nix; "how-to-fragments" = ./how-to/composition/fragments-cluster.nix; "how-to-multi-env" = ./how-to/composition/multi-env-cluster.nix; "how-to-package-storage" = ./how-to/package-stacks/storage-cluster.nix; "how-to-package-python" = ./how-to/package-stacks/python-cluster.nix; "how-to-package-stackgres" = ./how-to/package-stacks/stackgres-cluster.nix; "how-to-package-monitoring" = ./how-to/package-stacks/monitoring-cluster.nix; "how-to-package-cilium" = ./how-to/package-stacks/cilium-cluster.nix; "how-to-package-valkey" = ./how-to/package-stacks/valkey-cluster.nix; "how-to-package-velero" = ./how-to/package-stacks/velero-cluster.nix; "how-to-package-victoria-metrics" = ./how-to/package-stacks/victoria-metrics-cluster.nix; "how-to-platform-gateway" = ./how-to/platform/gateway-cluster.nix; "how-to-platform-hostpath" = ./how-to/platform/hostpath-cluster.nix; "how-to-platform-ingress" = ./how-to/platform/ingress-cluster.nix; "how-to-platform-monitoring" = ./how-to/platform/monitoring-cluster.nix; "how-to-platform-network-policy" = ./how-to/platform/network-policy-cluster.nix; "how-to-platform-secrets" = ./how-to/platform/secrets-cluster.nix; "how-to-platform-sops" = ./how-to/platform/sops-secrets-cluster.nix; "how-to-platform-cloudflare-tunnel" = ./how-to/platform/cloudflare-tunnel-cluster.nix; }; };}The first block, inputs, names the external Nix dependencies:
nixpkgsis the normal Nix package collection;kixpkgsprovides Kix itself and the package catalog used by these examples;kixpkgs.inputs.nixpkgs.follows = "nixpkgs";keeps both inputs using the samenixpkgs.
The second block, outputs, says what this repo provides. Kix projects usually
delegate that output wiring to inputs.kixpkgs.lib.mkFlake.
mkFlake is the Kix helper that turns the clusters map, such as
"02-hello-world" = ./tutorials/02-hello-world/cluster.nix, into the flake
outputs the CLI knows how to read.
A few Nix syntax notes, kept deliberately small:
{ ... }is an attrset, like a map or object.- Each assignment ends with
;. inherit inputs;meansinputs = inputs;.outputs = inputs: ...is a function. It receives the flake inputs and returns the project outputs.
Cluster Names Come From clusters
Section titled “Cluster Names Come From clusters”The attr name, 02-hello-world, is the cluster name you pass to the CLI:
❱ kix deploy 02-hello-world -y Show output
Building cluster '02-hello-world'...
Cluster 02-hello-world: 10 manifests
Connecting to cluster...
No previous activation on cluster — first deploy.
Reading live cluster state...
_cluster
~ cluster-level resources (4 added)
kube-system
+ platform-dns (0 resources)
tutorial-02
+ hello-world 1.27 (3 resources)
Plan: cluster-level changes, 2 added
Resources: 4 real content, 0 dep-affected
plan: 10 nodes
~ Namespace/kube-system configured
✔ Namespace/kube-system ready
+ Namespace/tutorial-02 created
✔ Namespace/tutorial-02 ready
+ CustomResourceDefinition/packageinstances.kix.run created
+ CustomResourceDefinition/activations.kix.run created
+ ConfigMap/hello-world@tutorial-02 created
✔ ConfigMap/hello-world@tutorial-02 ready
+ Deployment/hello-world@tutorial-02 created
✔ CustomResourceDefinition/packageinstances.kix.run ready
✔ CustomResourceDefinition/activations.kix.run ready
+ PackageInstance/platform-dns@kube-system created
✔ PackageInstance/platform-dns@kube-system ready
✔ Deployment/hello-world@tutorial-02 ready
+ Service/hello-world@tutorial-02 created
✔ Service/hello-world@tutorial-02 ready
+ PackageInstance/hello-world@tutorial-02 created
✔ PackageInstance/hello-world@tutorial-02 ready
+ Activation/02-hello-world-rh3pdaspsdxk created
✔ Activation/02-hello-world-rh3pdaspsdxk ready
• activation '02-hello-world-rh3pdaspsdxk' → Active
Deploy complete: 9 created, 1 configured, 0 unchanged, 0 failed ❱ kix pf 02-hello-world hello-world 8080:80 The value points at the file that builds that cluster.
clusters = { dev = ./clusters/dev.nix; staging = ./clusters/staging.nix; prod = ./clusters/prod.nix;};The names are yours. The CLI discovers them by evaluating the flake.
cluster.nix Defines What Is Installed
Section titled “cluster.nix Defines What Is Installed”The flake points at ./tutorials/02-hello-world/cluster.nix. That file is the
recipe for one cluster: it names the cluster, chooses the local kind flavor, and
declares what should exist inside the cluster.
At the top level, the file has this structure:
{ kix, packages }:
# `kix.buildCluster` builds a cluster spec from an attrset. The body# below is one big attrset literal.kix.buildCluster { name = "02-hello-world";
# `modules` is a list (whitespace-separated, no commas). modules = [ # Sets the Kix flavor. These examples use kind (Kubernetes in Docker). # Setting the flavor allows Kix to assume sensible defaults. kix.flavors.kind
# A module — an attrset describing part of the cluster. { # Every namespace the cluster manages. `{ }` is an empty attrset # — we don't need any per-namespace config here. namespaces = { tutorial-02 = { }; };
# `instances.<namespace> =` is the dotted-path shorthand for # creating a nested attrset. # Here we have an instance named `hello-world` in namespace # `tutorial-02`. instances.tutorial-02 = { # package instances go here }; } ];}mkFlake passes two useful arguments into this function:
kix, the framework helpers;packages, the package catalog fromkixpkgs.
That first line, { kix, packages }:, is Nix function syntax. For now, read it
as “Kix gives this file the helpers and package catalog it needs.”
The cluster then calls kix.buildCluster. Inside modules, the first entry,
kix.flavors.kind, says this cluster targets kind.
The second entry is a small cluster module that creates one namespace and leaves room for package
instances.
The package instance lives under instances.tutorial-02:
hello-world = { # `packages.echo-server` ships in the public kixpkgs # repository, along with many others. # It serves a simple fixed message over HTTP. package = packages.echo-server;
# `config` is whatever the package's options accept. # echo-server's `message` becomes the body of every response. config = { message = "Hello from Kix!"; }; };That instance defines four practical facts:
- namespace:
tutorial-02; - instance:
hello-world; - package:
packages.echo-server; - config: the message returned by the echo server.
That is enough for Kix to build Kubernetes manifests and enough for the CLI to
know there is a package instance named hello-world.
Where Packages Live
Section titled “Where Packages Live”There are two package sources to keep separate.
Catalog packages come from kixpkgs. In the first example,
packages.echo-server is a catalog package. You use it directly from the
packages argument.
Local packages live in your repo. The second example imports one:
let reverseProxyPackage = import ./reverse-proxy-package.nix;inYou will build a local package later in the tutorial path. For now, the rule of thumb is:
- put cluster composition in
cluster.nix; - put reusable package code in a separate package file;
- keep shared cluster modules in their own files once
cluster.nixgets too large.
That keeps “what is installed here?” separate from “how does this package build Kubernetes resources?”
How The CLI Uses These Files
Section titled “How The CLI Uses These Files”When you run a command from the repo root:
❱ kix list clusters
NAME
02-hello-world
06-service-dep
08-namespace-deps
09-typed-options
10-reuse-packages
19-scorecards
how-to-adoption
how-to-adoption-takeover
how-to-application
how-to-auto-instantiation
how-to-composition
how-to-fragments
how-to-helm-bridge
how-to-multi-env
how-to-package-cilium
how-to-package-monitoring
how-to-package-python
how-to-package-stackgres
how-to-package-storage
how-to-package-valkey
how-to-package-velero
how-to-package-victoria-metrics
how-to-platform-cloudflare-tunnel
how-to-platform-gateway
how-to-platform-hostpath
how-to-platform-ingress
how-to-platform-monitoring
how-to-platform-network-policy
how-to-platform-secrets
how-to-platform-sops
how-to-stateful-migration Kix evaluates the current directory’s flake. If you pass --flake, it evaluates
that flake instead:
❱ kix --flake /path/to/kix-examples list clusters From the flake outputs, the CLI can discover:
- cluster names;
- package catalog metadata;
- rendered manifests for a cluster;
- installed package instances inside a cluster.
That is why the command names in the previous tutorial were short. Once Kix has the flake and cluster name, it can find the rest.
What Comes Next
Section titled “What Comes Next”You now know what flake.nix contributes and what cluster.nix contributes:
the flake exposes clusters, and each cluster file says what to build.
The next tutorial uses these files to create the first cluster from scratch.