Using a GitHub Actions Pipeline For CI/CD to NixOS Machine
Coming from a DevOps background, I generally aim to automate deployments and server setups. When I switched to using NixOS, the allure of defining a machine via a single configuration file made achieving this CI/CD dream seem easy.
Unfortunately, like most things in NixOS, not everything is straightforward — you often have to dig and debug to figure out how to get things done. Luckily, I came across this post describing how to deploy NixOS on DigitalOcean, which was invaluable.
The Stack
My infrastructure setup is pretty straightforward. I have VMs running on DigitalOcean with NixOS installed, and my code resides in GitHub repositories. I decided to use GitHub Actions to achieve my goals.
The other thing I needed to figure out was how to update the configuration state on the NixOS machines. I considered writing my own script to SCP my files and then run the switch
command. This seemed like a ton of work. I’m trying to get websites and web applications running on the internet, not create some unique build system.
Finding Morph
At this point, I had heard of Morph but hadn’t gotten my hands dirty with it. It seemed like just the tool I was looking for. Unfortunately, just looking at the repo didn’t give me a clear idea of where to start.
Anatomy of a Morph Network
I’m no expert by any means—I just somehow got it to work for my use cases. Morph allows your configuration to be defined as a network
. This means you can describe multiple machines in the same configuration file.
This is an extremely simplified version of what a network configuration looks like. In this case, we define a web server running Nginx and a database running Postgres. webserver
and database
define separate machines when they’re listed under the network configuration options.
{
network = {
description = "simple network";
};
webserver = { modulesPath, lib, name, ... }:{
services.nginx.enable = true;
};
database = { modulesPath, lib, name, ... }: {
services.postgresql.enable = true;
};
}
Doing a Morph Deployment
Once we have a configuration defined for Morph we can move onto getting it up and running. The easiest way to get started is to create a nix shell environment of morph
. Nix shell provides an ‘isolated’ environment with the desired software.
nix shell nixpkgs/nixos-24.05#morph # Pin to the version of nixpkgs
Now that morph is installed, we can go ahead and update our machines defined in our network file. We do this using the switch
command.
morph deploy network.nix switch
We can make this all a oneliner. I do the following:
nix shell nixpkgs/nixos-24.05#morph --command sh -c "morph deploy network.nix switch"
Diving Into the Repo
I create a seperate repository to manage just the infrastructure. Simplified it’s made up of 3 files. We have our network.nix
that defines all of our machines running NixOS. As my_hosts
that contains the IP adresses of our digital ocean droplets and our GitHub actions yaml located at .github/workflows/deploy.yml
.
Our GitHub Repo Layout
├── .github/workflows/deploy.yml # Github Actions Yaml
├── my_hosts # Helper hosts file
└── network.nix # Nix Configuration
Github Actions Pipeline
The GitHub actions yaml is pretty simple. We need to accomplish a few things such as install NixOS on our deployment machine, add our SSH keys and then run our deployment commands.
Adding an SSH Key to Our Runner Machine
Using the my_hosts
file we created previously and the following snippet, we can enable our GitHub actions runner to connect to our host machines to do our deployment. Make sure to add your SSH_KEY
as a actions secret or else this step will not work.
- name: Add SSH key
env:
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
run: |
mkdir -p /home/runner/.ssh
ssh-keyscan -f my_hosts >> /home/runner/.ssh/known_hosts
echo "${{ secrets.SSH_KEY }}" > /home/runner/.ssh/github_actions
chmod 600 /home/runner/.ssh/github_actions
ssh-agent -a $SSH_AUTH_SOCK > /dev/null
ssh-add /home/runner/.ssh/github_actions
Complete Github Actions Yaml
Using the steps we discussed we can define our GitHub actions pipeline to automate running an update when a push to master branch is made to our GitHub repository.
name: nixos-deployment
on:
push:
branches:
- 'master'
workflow_dispatch:
branches:
- 'master'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Add SSH key
env:
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
run: |
mkdir -p /home/runner/.ssh
ssh-keyscan -f my_hosts >> /home/runner/.ssh/known_hosts
echo "${{ secrets.SSH_KEY }}" > /home/runner/.ssh/github_actions
chmod 600 /home/runner/.ssh/github_actions
ssh-agent -a $SSH_AUTH_SOCK > /dev/null
ssh-add /home/runner/.ssh/github_actions
- name: Install Nix On Runner
uses: cachix/install-nix-action@v30
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Run Morph Nix Deploy
env:
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
run: nix shell nixpkgs/nixos-24.05#morph --command sh -c "morph deploy network.nix switch"
Trying it Out
Go ahead and try this out on your own project. There are a lot of places to go from here. You can run test cases, update downstream infrastructure with your new container tag and even expand this for test branches.
I write about devops releated content. If your into that sort of thing, check out out how to containerize a Django application and use that container in NixOS.