Homelab deployment

David Wagner

I configured continuous deployment in my homelab. This article describes how I use Nix with GitHub Actions and Cachix Deploy to automatically deploy NixOS machines on my home network.

Overview

My home network comprises a wireless router, a few computers, temperature and humidity sensors and remote controllable switches. I configure these devices mainly using Nix, and when it’s possible, I run NixOS on them. I use this setup to experiment with home automation and to learn about new tools. The entire configuration of my home network is available on GitHub.

The configuration of the NixOS servers was always built from a declarative specification stored in version control system. But the deployment of the new configuration was manual, slow and often tedious. I ran nixos-build from the command line, and sometimes I had to wait an hour to build and deploy the new machine configuration.

I wanted to reconfigure my servers automatically, triggered by committing to a Git repository.

To solve this problem I built a system based on Nix and freely available hosted services:

Figure1

The automatic deployments work as follows:

  1. The Homelab repository contains the NixOS host configurations.
  2. A workflow in GitHub Action builds the NixOS system and stores it in a hosted binary cache.
  3. An agent on the target machine pulls the built binaries from the cache and activates the new deployment.

Host configuration

The configuration of my servers is written in the Nix language. For example, host-nuc.nix describes the hardware and software configuration of my Intel NUC device:

{ config, ... }:

{
  imports = [
    ./hardware/nuc.nix
    ./modules/cachix.nix
    ./modules/common.nix
    ./modules/consul/server.nix
    ./modules/git.nix
    ./modules/grafana
    ./modules/loki.nix
    ./modules/mqtt.nix
    ./modules/prometheus.nix
    ./modules/push-notifications.nix
    ./modules/remote-builder
    ./modules/traefik.nix
    ./modules/vpn.nix
  ];

  system.stateVersion = "22.05";
}

The configuration is split into modules. Glancing at the imports block you can tell what is installed on this machine. The host nuc is my main home server, it runs all my “production” services.

For example, the cachix.nix module configures the Cachix Agent which is responsible for reconfiguring the NixOS machine when a new build is available. The agent runs on all machines whose configuration automatically managed.

Building with GitHub Actions

The NixOS host specifications are built with a single nix build command:

nix build .#nixosConfigurations.nuc.config.system.build.toplevel

This builds the whole system for the nuc machine: the kernel, the installed packages and their configuration. By default, nix downloads pre-built packages from the NixOS public binary cache, so a typical execution of the build command takes only a few minutes.

To run the build in GitHub Actions, the build job installs Nix and configures the access to the Cachix binary cache where the deployment artifacts are stored:

jobs:
  build:
    runs-on: ubuntu-latest
    environment:
      name: Homelab
      url: "https://app.cachix.org/deploy/workspace/lab.thewagner.home/"  # ⑴
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-qemu-action@v3                                 # ⑵
      - uses: cachix/install-nix-action@v23
        with:
          extra_nix_config: "extra-platforms = aarch64-linux"             # ⑶
      - uses: cachix/cachix-action@v12                                    # ⑷
        with:
          name: wagdav
          authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
      - run: nix build --print-build-logs .#cachix-deploy-spec            # ⑸
      - run: |                                                            # ⑹
          cachix push wagdav ./result
          cachix deploy activate --async ./result
        env:
          CACHIX_ACTIVATE_TOKEN: "${{ secrets.CACHIX_ACTIVATE_TOKEN }}"

The build job of the workflow:

  1. Configures a deployment target. When a GitHub Actions workflow deploys to an environment, the environment is displayed on the main page of the repository.
  2. Installs the QEMU static binaries for building packages for architectures different than that of the build runner.
  3. Configures Nix to use emulation to build ARM 64 packages.
  4. Configures the Cachix hosted binary cache.
  5. Builds the deploy specification which is a set of the NixOS systems to deploy.
  6. Pushes the built binaries to the cache and sends an activation signal to the Cachix Agent.

The job uses two secrets: CACHIX_AUTH_TOKEN is the authentication token to push to the binary cache and CACHIX_ACTIVATE_TOKEN is required to activate the built NixOS configurations.

Deploying with Cachix Deploy

To deploy the built NixOS configurations I use the generous free-tier of Cachix Deploy. Following their documentation, I installed cachix-agent on the target hosts and configured a few authentication keys.

The agent process connects to the Cachix backend and waits for a deployment. When a new deployment is available, the agent pulls the relevant binaries from the binary cache and reconfigures the NixOS system it runs on.

Summary

Since I configured automatic deployment of this blog I wanted the same for my home infrastructure. I had my configuration repository and I knew how to build the machine configurations with GitHub Actions. I was missing the automatic deployment part until Cachix Deploy was announced. Cachix has excellent documentation and the integration with my Homelab was super simple.

Acknowledgement

I’m grateful to Cachix Deploy for offering a binary cache and a deployment service.