Deploy a NixOps Network

This guide will show you how you can roll out a NixOps configuration when a branch is updated.

When an agent deploys itself, it interrupts its deployment. This is might cause issues with NixOps' state file, so using NixOps to deploy your agents is not recommended for now. This will be improved.
Usage of secrets inside a NixOps deployment has not been documented yet, because a standard solution is under development.


  • You have set up an agent for the account that owns the repository

  • You have deployed a NixOps network before

  • You have added a repository to your Hercules CI installation

  • You have added the hci command

Import state file (optional)

If you want to deploy an existing network, you need to import the state file in order to give access to the existing machines and resources.

First, find the network you want to automate.

$ nixops list
| UUID                                 | Name                             | Description                 | # Machines |   Type  |
| e8c96407-026f-11eb-8821-024213d29dcb | foo                              | Unnamed NixOps network      |          0 |         |

Then, upload the state. State files identified by the project and file name.

$ nixops export -d e8c96407-026f-11eb-8821-024213d29dcb \ (1)
    | hci state put --project github/neat-org/neat-repo \ (2)
        --name nixops-foo.json \ (3)
        --file -
1 replace by the UUID of your deployment
2 replace by the project path that matches your repository
3 replace foo by the name of the deployment

Add runNixOps to ci.nix

This guide helps you write the ci.nix file in steps.

Let’s start with a basic invocation of runNixOps:

  # replace hash or use different pinning solution
  nixpkgs = builtins.fetchTarball
  pkgs = import nixpkgs {
    system = "x86_64-linux";
    overlays = [
      (import "${effectsSrc}/overlay.nix")

  # update hash if desired or use different pinning solution
  effectsSrc = builtins.fetchTarball

  inherit (pkgs.effects) runNixOps runIf;
  inherit (pkgs) lib;

  neat-network = runIf true (
    runNixOps {
      name = "foo";
      src = lib.cleanSource ./.; (1)
      networkFiles = ["network.nix" "network-aws.nix"]; (2)
1 Unlike some other deployment methods, NixOps needs access to the actual expression files.
2 For the same reason, we specify the expression files as path strings relative to src.

Add secrets to your agents

For example, with writeAWSSecret

runNixOps {
  # ...
  userSetupScript = ''
    writeAWSSecret aws default
  secretsMap."aws" = "neat-aws";

and add to secrets.json on your agents:

  "neat-aws": {
    "kind": "Secret",
    "data": {
      "aws_access_key_id": "AKIA.....",
      "aws_secret_access_key": "....."

Prebuild the network

To make sure all packages are cached, create a network file to replace any values that are unknown when the network configuration can’t access secrets or cloud resources.

Create an empty network file network-stub.nix:


and add it to prebuildOnlyNetworkFiles:

runNixOps {
  # ...
  networkFiles = ["network.nix" "network-aws.nix"];
  prebuildOnlyNetworkFiles = ["network-stub.nix"];

Then iterate on network-stub.nix until nix-instantiate ci.nix -A neat-network.prebuilt succeeds.

See deployOnlyNetworkFiles if you have overrides that you only want to use during continuous deployment.


Commit any remaining changes and push your branch. Your agents will build and deploy your network.

Meanwhile, you can configure which branch causes your deployment to run. For example, if you only want to deploy when you’ve merged into the production branch, use:

# Make ci.nix a function with default argument
{ src ? { ref = null; }}:

# ...

  neat-network = runIf (src.ref == "refs/heads/production") (
    runNixOps {
      # ...


After push/PR/merge, your continuous deployment is ready for use.