~Talks

NixCon 2026

27 September 2026 Kraków, Poland

Nix for Cloud Native Platform Engineering: From Imperative Templating to Functional Derivation

slides.pdf

youtube.com

Abstract

The Kubernetes ecosystem currently lacks a unified model for pure and reproducible deployments. Industry standards like Helm and Dockerfiles rely on impure templating and imperative definitions, creating non-deterministic side effects and opaque dependency trees. Often, the industry attempts to mitigate this by adding layers of orchestration (Internal Developer Platforms, GitOps controllers), which can inadvertently increase complexity without addressing the underlying fragility.

This talk demonstrates how to apply Nix’s functional purity to this landscape. By organizing a platform’s entire catalogue as a Nix monorepository, inspired by the architecture of nixpkgs, we can shift from managing state to deriving it, achieving total reproducibility for the entire cloud-native stack.

We will explore how Kubenix leverages the OpenAPI specification to treat Kubernetes manifests and Helm charts as Nix expressions, replacing traditional YAML templating with a strongly-typed approach. To address the build layer, we examine how dockerTools brings Nix’s hermetic, network-isolated sandboxing to the OCI ecosystem. By treating containers as first-class dependencies, we can ensure fully reproducible artifacts, optimize storage via shared OCI layers, and generate accurate SBOMs and provenance attestations as natural byproducts. Finally, we will see how the NixOS module system serves as a flexible composition layer for the scalable orchestration of complex cluster configurations.

Let’s stop templating our cloud-native infrastructure and start deriving it with Nix: purely functional, hermetically sealed, and truly reproducible.

Transcript

Intro

Hello everyone, and thank you for having me.

Today I want to show you what Nix can do for cloud-native platform engineering.

If you’d like to follow along or reference these slides later, just scan this QR code or visit the link provided.

echo $(whoami)

Before we get started, let me quickly introduce myself.

My name is Arik Grahl and I am a Staff Systems Architect at SysEleven, a cloud service provider based in Berlin. We have a quite uncommon main use-case for Nix: We are using it to package workloads for our managed Kubernetes offering running in our OpenStack cloud.

If anything in this talk sparks your interest, feel free to connect with me via any of the channels listed here. I’m always happy to chat!

NixOS vs. Kubernetes

To understand differences and similarities in packaging between NixOS and Kubernetes, let’s compare both operating systems.

NixOS is host-centric and usually scoped to a single instance. As a Linux-based operating system it comes with a Linux kernel. On top of this there are user-space processes, which may or may not be managed by systemd. Nix helps to keep track of dependencies like shared libraries and it’s the NixOS module system, which enable us to compose complex systems like a Linux-based operating system in the first place.

Kubernetes on the other hand is an operating system as well, but a distributed one. So it assumes a set of multiple nodes, mainly Linux systems as well. To manage a distributed state, Kubernetes has an API server with which the nodes communicate, a set of controllers, a scheduler and an etcd data store. As a container orchestration platform, Kubernetes primarily deals with sets of containers, called Pods, as its atomic unit of workloads. If we are talking about dependencies in Kubernetes, we usually speak about the relation between services. This is where Kubernetes excels in the end: Scaling stateless microservices.

Cloud Native

With the distinction from a traditional Linux operating system in mind, we can now turn our attention to the strengths and weaknesses of Kubernetes.

In my eyes its overall API architecture is great: the reconciliation towards a declared state is well-suited for a constantly drifting world, and the API provides a reasonable abstraction, intentionally leaving out vendor or hardware-specific concerns. Since it is also extensible, vendor lock-in stays minimal. I would argue it was never easier to move your workloads across different cloud providers.

It gets a little bit more challenging when it comes to controlling complexity. Not a problem introduced by Kubernetes, which is naturally deployed for use-cases with inherently high complexity. But it drives an ecosystem where users develop a habit of stacking applications without introducing any abstraction or addressing the root issue. Maintaining this often self-introduced complexity makes a sophisticated CI/CD system with its own logic mandatory, or even an internal developer platform.

What bothers me the most about the whole cloud-native ecosystem is how packaging is thought. Since for end users Kubernetes is basically exposed as an API server, packaging boils down to modifying YAML. It comes in different flavors: Kustomize mutates base documents with a JSON-patch-like approach: quite minimal, but with imperative semantics, contrasting the otherwise declarative world. Helm, the de facto and probably most popular package manager, is more declarative, but it is just a sophisticated YAML templating engine, so there is no real type safety during build time. KRO addresses this with schema-based generation, but requires in-cluster components and does not allow breaking changes in already-defined schemas.

Cloud Native Computing Foundation (CNCF) Landscape

So let’s take a look at the landscape that has developed over the years against this background.

You are not expected to be able to read the names of all of these projects. My point here is: There are a lot of projects emerging and I haven’t even included all categories in this visualization. I am not saying that the quality of these projects is bad, rather the opposite is true. I am also not saying that all of these projects should be obsolete, because they are solving the wrong problems.

Stacked Mitigations Instead of Solving the Root Cause?

But at some point, I think it is valid to raise the question: Are we just stacking one fix on top of another instead of actually addressing the root cause?

Earlier this year, I had the pleasure of hearing Kelsey Hightower at a conference in London. When he looked at the exhibitors at the conference, he made the observation that “half of the vendors are solving problems Kubernetes helped to create”.

In the end everyone needs to decide for themselves if all of the CNCF landscape is relevant. But today I want to showcase how we can use Nix to build cloud-native platforms, which have a potentially higher quality with less complexity, thus making some of the projects obsolete for our scenario.

Kubenix (1/2)

So without further ado, let’s jump right into it. The first and probably most important pillar of Nix-powered cloud-native platform engineering is Kubenix.

At its core it mainly implements the OpenAPI specification of the Kubernetes API. In our Nix expression, we can make use of this by just importing its NixOS module. Since the structure of the Kubernetes resources and its potential default values are known, we gain a lot of expressiveness with relatively few lines of code. For instance we can define a Pod simply by specifying its container image in combination with the well-known Nix language features. Everything completely declarative and without templating. One of Kubenix’s best properties is probably type safety during build time. If we for instance define the containerPort with the wrong type, Kubenix’s OpenAPI specification makes us aware that this is intended to be an integer, the same would apply for unknown attributes. Helm would protect us from neither of these issues.

Kubenix (2/2)

You might have already noticed that Kubenix is effectively leveraging NixOS modules to define Kubernetes manifests. This allows us to achieve meaningful inheritance and flexible composability. With just the basic Nix language features we can express complex data structures like the labels here. The module system then allows us to compose complex application stacks, for instance an application consisting of a deployment, service and HTTP route, reusing the previously defined variables.

But Kubenix doesn’t stop there. It comes with a Helm wrapper, which allows us to interface with the large Helm ecosystem from inside our Nix world. So we can simply define our Helm releases for instance by fetching the Helm chart and passing the values as a basic Nix attribute set. Kubenix doesn’t limit us to Helm in isolation: we can mix and match Helm releases with other Kubernetes resources, and Kubenix merges everything into a consolidated manifest, even detecting colliding resources at build time to prevent accidental overwrites. Since we can overwrite arbitrary resources, we are no longer bound to the chart’s values or its templating.

In short: Kubenix opens up the entire Helm chart ecosystem to you, on your own terms.

OCI: Docker vs. Nix

The second fundamental pillar of our Nix-powered cloud-native platform engineering deals with containers themselves.

Therefore, let’s start by having a brief look at an example Dockerfile, where traditionally containers are built from. As you can see, this is a series of imperative steps whose order matters and since the standard build tooling is potentially not deterministic, the build is not reproducible. A container is intended to be self-contained, therefore, the scope of dependencies is per OCI image and there is no explicit way to reuse them across different images. Often OCI images are not minimal, because they include build dependencies or even complete package managers.

Nix also comes with container builders, so let’s have a look at the equivalent container expressed via buildLayeredImage. Thanks to Nix this is completely declarative and stateless. There is no order of imperative steps, which is key for a reproducible build. Since Nix is aware of its dependencies it can map each of these to a distinct OCI image layer, which even enables us to share dependencies across different images. The declarative fashion also contributes to a minimal OCI image per design, because it tends to not include build dependencies and unused artifacts.

OCI: buildLayeredImage Reuses Shared Dependencies

Because the property of potentially sharing dependencies across the boundaries of OCI images is of such great importance, let’s have a more detailled look here.

The initial situation is the same Nix expression as before, building an Nginx OCI image. In the same way, we can also containerize other applications, such as Redis.

We can now build the first Nix expression and have a look at its OCI image representation, for instance with podman save. We can see that we get a blob containing the Nginx binary, which is also referenced as the container’s Entrypoint. In fact, each dependency translates to a specific blob in the image.

Let’s build the other Nix expression, representing the Redis OCI image. Here as well, we have a blob holding the application-specific binaries, like the redis-server binary. Also this OCI image has a blob for each dependency. And the twist is: As you might already have spotted by the identical hashes, they are exactly the same.

Therefore, when consuming both these OCI images we can completely reuse all of these layers representing shared dependencies. This is a huge gain for pulling the images and for the container boot time, and it also improves the disk usage of the container runtimes. In theory, traditional Docker builds could achieve the same, manually and error-prone. The Nix tooling takes care of everything per design, no extra efforts needed.

OCI: Integration of buildLayeredImage

I think the advantages of Nix-based containerization are obvious. But how can we integrate this tooling into the Kubernetes ecosystem, ideally in a holistic way? There are different solutions to it, but I want to demonstrate server software, which I call Nix OCI, which removes the need to push your OCI artifacts over to a registry.

When building our well-known Nix expression, we receive an OCI image in the Nix store. This is the server side, on the client side, we want to consume this container, for example by a Pod. The container runtime requests the manifest for the OCI image from the server, which answers successfully since the corresponding file is present. To serve the manifest, the server inspects the archive and serves the manifest. The client fetches it again, now addressed by a digest. Then it requests the configuration blob, followed by the first binary layer. This continues until all remaining layers are served.

The Nix OCI server is basically a fully-compliant but read-only OCI registry backed solely by a local Nix store. Since it leverages standard OCI protocols, it is very flexible to use.

Monorepository: flake.nix

With Kubenix and buildLayeredImage as our two main pillars for Nix-based cloud-native platform engineering at hand, we can now compose everything together. Inspired by Nixpkgs, we are therefore using NixOS modules organized in a mono repository. In this example, I am showcasing a flake.nix, but you are not forced to use Nix flakes.

The main idea is that all upstream dependencies translate into the inputs: the tooling itself, like Kubenix, the Helm charts we want to consume, like cert-manager, and software repositories for Nix-based containerization.

The outputs in turn reflect our composition layers. This can be whatever makes sense for you, in this example I am sketching my production environment. Essentially we are evaluating NixOS modules, for instance with kubenix.evalModules. We can make use of a very flexible and safe configuration, for instance our cert-manager module exposes an enable option, just the way we like it. An evaluated NixOS module is finally converted to a structured document. With resultYAML for instance we get a multi-document YAML output.

Monorepository: modules/cert-manager.nix (1/2)

With the overall mono repository structure in mind, we can now have a closer look at one of our NixOS modules.

As illustrated before, the module options basically exposes the configuration of the subject being packaged. In this example we introduce the common enable option, along with an option representing the default Kubernetes namespace of cert-manager.

The first responsibility of the module’s config is to define the dependencies by means of other modules. Dependencies of a packaged Kubernetes workload could be for instance custom resource definitions. In our cert-manager example we therefore enable cert-manager’s own CRDs along with the CRDs of Gateway API. Other dependencies could involve OCI images. Through another NixOS module for OCI images, we are defining a list of OCI images as dependencies for cert-manager.

Monorepository: modules/cert-manager.nix (2/2)

The second responsibility of the module’s config is of course to define the actual payload of the package. Usually, one would expose an evaluated Helm chart here, just like we have seen for the Kubenix examples before. For our cert-manager example this could look like this: Guarded by the usual utility functionality we define our Helm release based on a chart derivation and its values.

We would also need to wire our previously referenced OCI images as dependencies here. Therefore we select the respective Kubernetes deployments and override their images.

And that’s basically all it takes to package a Kubernetes workload as a NixOS module. You might have noticed that the OCI image URL got automatically resolved based on the previously defined dependencies.

Monorepository: modules/oci.nix (1/2)

Therefore, let’s have a look at how the NixOS module to resolve OCI images could look like.

Under its module options we are able to configure the registryBaseURI. This would be the location of our previously introduced Nix OCI registry. We also have the images option to register OCI images. These are keyed by their path, so they receive an OCI image as input, for example buildLayeredImage. The last option, resolved, would expose previously registered OCI images as fully compliant URLs. In this example I have implemented it with internal = true and I use submodules to resolve parts of the URL such as the repository or tag for convenience.

Monorepository: modules/oci.nix (2/2)

In the module’s config this is also the central field which gets populated. It consumes the previously registered images, here in this example represented by the argument named archive. It parses the structure of the archive’s filename as implemented by this regex matcher. Then the matched parts are exposed as structured data: archive, hash, tag, and name. It also takes the registryBaseURI into account, for instance when constructing the repository and the complete url.

With this fully generic module for our convenience, we can easily wire previously defined OCI image dependencies into other NixOS modules representing Kubernetes workloads.

Coherent and Simplified Tooling for Kubernetes (1/2)

The best thing about our Nix mono repository is that all of the tooling is very streamlined. Whoever has worked with some cloud-native tooling knows what I am referring to: The whole build process might become incoherent and hard to understand.

With Nix we can rely on the established tooling, for instance use nix flake show to list our mono repository. We can expose the previously written NixOS modules representing packages of Kubernetes workloads to make them consumable from outside of our repository. This becomes handy for library modules such as CRDs.

We can also expose our OCI images as packages to build them explicitly from outside this repository. And last but not least, here we have our composition layer representing our example production environment.

Coherent and Simplified Tooling for Kubernetes (2/2)

If we now build this package, it will automatically build all of its dependencies. Including CRDs and all of its container images. All without using any tool other than Nix.

The output is ready to be applied as it is. Because this is so simple we are very flexible in how we can use it. For sure it works from a CI/CD environment and can be used for GitOps workflows. But it does not obfuscate the builder logic there.

Software Supply Chain: Dependency Graph

The coherent tooling also contributes positively to software supply chain aspects. Since we have carefully defined the dependencies we can generate meaningful dependency graphs.

The sbomnix project is a tool collection for such use-cases. With the nixgraph tool we can for instance analyze a different output in our repository, representing a staging environment. We can see that it is referencing Redis and Nginx OCI images as dependencies. With the --depth argument we can specify how deep we want to look into the dependency graph. The next level yields the actual payload of the container images, the Redis and Nginx packages from Nixpkgs. Another level shows the dependencies of these packages and highlights which of them are shared.

Software Supply Chain: SBOM and SLSA

The sbomnix collection has of course also tooling to generate machine-readable reports.

With sbomnix we can generate software bill of materials in common formats such as SPDX or CycloneDX here. We can filter for runtime and build-time components and we can see that our staging environment consists indeed of nginx, glibc, openssl and others.

There is also tooling for SLSA provenance attestation. Analyzing our example stack from before will show the same dependencies like nginx and glibc and naturally also build-time dependencies such as gcc.

Thanks to Nix and our efforts to define dependencies upfront, we get a lot of software supply chain security for free and it integrates well with existing tooling.

Conclusion (1/3)

As we’re reaching the end of the presentation, it’s time to draw a conclusion.

With Kubenix we gain the ability to define Kubernetes resources with type safety during build time. The underlying OpenAPI specification conveniently also reduces the boiler-plate code for us. Thanks to the interfacing with Helm, we can consume packages from Kubernetes’ largest ecosystem.

Conclusion (2/3)

Now our second pillar: Nix-based OCI containers.

Instead of relying on imperative and unreproducible builds, Nix allows us to declare container images in a fully reproducible fashion. Since it translates each dependency to a container layer we can potentially share dependencies beyond the boundaries of OCI images. Nix does not include build dependencies in the containers and the declarative fashion tends to only cover what’s necessary, which is why we get potentially small OCI images.

Conclusion (3/3)

The third pillar of this architecture deals with the proven and flexible NixOS module system.

Kubenix leverages it already to define Kubernetes resources and we can define our own to describe high-level services. This is the main driver to streamline composability and enables us to define complex application stacks. Not only can we package many applications, but we can also do so to the desired depth, giving us full control through Nix-based containers. Based on this feature of the end-to-end definition, we gain significant insights into the software supply chain, and this explicit knowledge gives us a valuable security advantage.

Questions & Answers

This brings me to the end of my presentation.

If you’d like to go over the slides once more, feel free to scan this QR code. If you are interested in Nix for cloud native, you might want to check out the material I have collected on my website.

I guess we are running out of time for questions. So just come find me in the hallway.