How to generate NixOS LXC templates in GitHub Actions

Published

I started using Nix some time ago when I got this M2 MacBook Air. With nix-darwin and home-manager, it’s a great way to handle dotfiles.

The thing with Nix is, when you start using it somewhere, you start using it everywhere. There is no turning back to entering commands one after the other to install and configure programs.

Naturally, when I started homelabbing with Proxmox (PVE) as my hypervisor, I used NixOS to create LXC containers, which are bootstrapped using a template.

While you could pull a NixOS container template from Hydra, install it in PVE, and pull your configuration from your Nix config repo, what if you could instead generate a pre-built NixOS LXC template direclty from you repo?

That’s exactly what I ended up doing, using GitHub Actions and nixos-generators.

Solution

Here’s my GitHub Actions workflow to generate NixOS container templates. I’ve commented for clarity.

name: 'generate-lxc'

# The workflow is triggered on new version tags with a hostname suffix,
# where the hostname corresponds to a nixosConfiguration in flake.nix.
on:
  push:
    tags:
      - 'v*.*.*-*'

# softprops/action-gh-release@v1 needs these permissions
permissions:
  contents: write

jobs:
  generate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Extract the hostname from the version tag suffix and
      # save it as an environment variable.
      - name: Extract tag suffix
        run: echo "TAG_SUFFIX=${GITHUB_REF#refs/tags/*-}" >> $GITHUB_ENV

      - name: Install nix
        uses: cachix/install-nix-action@v24
        with:
          github_access_token: ${{ secrets.GITHUB_TOKEN }}

      # Use the tag suffix to specify which configuration to build with the generator,
      # and then store the build path in an environment variable.
      - name: Generate NixOS configuration
        run: |
          nix run github:nix-community/nixos-generators -- -f proxmox-lxc --flake .#${{ env.TAG_SUFFIX }} | {
            read path
            echo "BUILD_PATH=$path" >> $GITHUB_ENV
          }

      # Move the build artifact to a working directory and
      # rename it to include the tag suffix.
      - name: Prepend tag suffix to file name
        run: |
          NEW_FILENAME="${{ env.TAG_SUFFIX }}-$(basename ${{ env.BUILD_PATH }})"
          RELEASE_PATH="${{ github.workspace }}/$NEW_FILENAME"
          cp "${{ env.BUILD_PATH }}" "$RELEASE_PATH"
          echo "RELEASE_PATH=$RELEASE_PATH" >> $GITHUB_ENV

      # Create a GitHub release and attach the generated container template.
      - name: Release
        uses: softprops/action-gh-release@v1
        with:
          files: ${{ env.RELEASE_PATH }}

This assumes you’re using flakes, and have a flake.nix file at the root of your repo.

Here’s what nixosConfigurations should look like:

{
  # ...

  outputs = { darwin, home-manager, nixpkgs, ... }: {

    darwinConfigurations = {
      # macOS config
    };

    nixosConfigurations = {
      "nfs" = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          "${nixpkgs}/nixos/modules/virtualisation/proxmox-lxc.nix"
          ./nixos
          ./nixos/nfs.nix
        ];
      };

      "dns" = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          "${nixpkgs}/nixos/modules/virtualisation/proxmox-lxc.nix"
          ./nixos
          ./nixos/dns.nix
        ];
      };
      # ... other configurations
    };
  };
}

For instance, if I want to build the nfs system, I commit and push my changes, then tag it as v0.2.2-nfs. For dns, I would tag it v0.1.0-dns. Here’s what the releases page look like:

GitHub repo releases page

If your repo is public, you can then use the Download from URL button in the PVE user interface and give it the tarball URL direclty. Otherwise, download it locally and upload it to PVE manually.

And that’s it! When you create a Linux container using this template, you’ll have a NixOS machine ready to go!