OFFSET偏移
← All articlesOFFSET / FIELD NOTESNO. 072

Deploying NixOS over SSH

Distinguish local builds, temporary remote activation, persistent switching, and failure rollback.

English translation · Code and commands follow the Chinese original.

IN THESE READING PATHSNixOS virtualization and deployment ↗
About 5 min readStart reading

Version scope: The examples below retain the tutorial's NixOS 24.05 context; that release is no longer maintained. Real projects should choose a supported release, pin the nixpkgs revision, and verify the corresponding options. system.stateVersion specifies a compatibility baseline and should not be changed mechanically with each upgrade. The commands in this article are teaching examples and have not been run or verified on a target system.

Translated from nix.dev: Provisioning remote machines via SSH

Deploy NixOS to remote servers over SSH to implement infrastructure as code.

Prerequisites#

  • A target server with NixOS already installed
  • SSH key access
  • Nix installed locally

Basic Deployment Workflow#

1. Prepare the Target Server

Ensure SSH is enabled on the remote server:

# 在远程服务器上检查
systemctl status sshd

2. Build the Configuration Locally

Create a deployment configuration, deployment.nix:

{ config, pkgs, ... }:

{
  # 网络配置
  networking.hostName = "remote-server";
  networking.firewall.enable = true;
  networking.firewall.allowedTCPPorts = [ 22 80 443 ];
  
  # SSH 配置
  services.openssh = {
    enable = true;
    settings = {
      PermitRootLogin = "no";
      PasswordAuthentication = false;
    };
  };
  
  # 用户配置
  users.users.deploy = {
    isNormalUser = true;
    extraGroups = [ "wheel" ];
    openssh.authorizedKeys.keys = [
      "<DEPLOY_SSH_PUBLIC_KEY>"
    ];
  };
  
  # Web 服务示例
  services.nginx = {
    enable = true;
    virtualHosts."example.com" = {
      root = "/var/www";
      locations."/" = {
        extraConfig = "index index.html;";
      };
    };
  };
  
  system.stateVersion = "24.05";
}

3. Deploy Remotely with nixos-rebuild

The target must already have NixOS installed, and the deployment configuration should also import that target's hardware-configuration.nix, filesystem configuration, and boot modules. The service fragment here is not a complete bare-metal configuration. First keep a working SSH session and console recovery path available, and confirm that the deploy user can elevate to root. The 24.05 rebuild uses --use-remote-sudo; the equivalent in newer rebuild-ng versions may be --sudo. Consult the target tool's --help.

Build first, then test. test modifies the running system but does not make it the default for the next boot. switch both activates the configuration and updates the system generation/boot configuration.

# 构建,不激活目标
nixos-rebuild build -I nixos-config=./deployment.nix
# 实际远程临时激活,先确认影响再执行
nixos-rebuild test --target-host deploy@remote-server --use-remote-sudo \
  -I nixos-config=./deployment.nix
# 远程部署并应用配置
nixos-rebuild switch \
  --target-host deploy@remote-server \
  --use-remote-sudo \
  -I nixos-config=./deployment.nix

Arguments:

  • --target-host: target server address
  • --build-host: specifies a remote build host; when omitted, builds normally run locally. Do not assume that the special value localhost behaves consistently across versions.
  • --use-remote-sudo: enables remote sudo for a non-root target user in the historical rebuild implementation. Whether a password can be entered interactively depends on the tool version and connection method.

Deploying with Flakes#

flake.nix

{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
  
  outputs = { self, nixpkgs }: {
    nixosConfigurations.remote-server = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [ ./configuration.nix ];
    };
  };
}

Deployment Command

# 构建并部署
nixos-rebuild switch \
  --flake .#remote-server \
  --target-host deploy@remote-server --use-remote-sudo

Deploying Multiple Servers in a Batch#

The following script actually activates servers one at a time. Each nixosConfigurations key must exactly match its corresponding name in the array. Complete build/test and SSH verification on a single node before approving the full list. A failure stops subsequent deployments; nodes already deployed successfully are not automatically rolled back as a group. Create deploy.sh:

#!/usr/bin/env bash

SERVERS=(
  "server1.example.com"
  "server2.example.com"
  "server3.example.com"
)

for server in "${SERVERS[@]}"; do
  echo "Deploying to $server..."
  nixos-rebuild switch \
    --flake ".#$server" \
    --target-host "deploy@$server" --use-remote-sudo || exit 1
  echo "✓ $server deployed"
done

echo "All servers deployed!"

Using deploy-rs#

deploy-rs is a community-maintained deployment tool with support for atomic deployment and rollback.

Installation

# 在 shell.nix 或配置中添加
environment.systemPackages = with pkgs; [
  deploy-rs
];

Configure deploy.nix

The attrset below should be the flake's deploy output, with self and the deploy-rs input in scope. Simply saving it as an isolated file does not integrate it with the tool. Also consider exposing deploy-rs.lib.<system>.deployChecks self.deploy under checks. Atomic profile switching, rollback on failure, and magic rollback all depend on configuration and SSH recovery conditions; they do not replace data backups.

{
  nodes = {
    web-server = {
      hostname = "web.example.com";
      profiles.system = {
        user = "root";
        sshUser = "deploy";
        path = deploy-rs.lib.x86_64-linux.activate.nixos 
          self.nixosConfigurations.web-server;
      };
    };
    
    db-server = {
      hostname = "db.example.com";
      profiles.system = {
        user = "root";
        sshUser = "deploy";
        path = deploy-rs.lib.x86_64-linux.activate.nixos 
          self.nixosConfigurations.db-server;
      };
    };
  };
}

Run the Deployment

# 部署所有节点
deploy

# 部署特定节点
deploy .#web-server

# 显示待激活变化:可能构建、上传并在远端 dry-activate,仍会使用远程连接
deploy --dry-activate

# 只做本地 flake 检查与系统构建,不触碰目标
nix flake check
nix build .#nixosConfigurations.web-server.config.system.build.toplevel

Managing Secrets#

Do not put plaintext secrets in Nix strings, the store, images, or Terraform state. agenix-encrypted files can be managed alongside configuration, while private keys remain at controlled paths on the target. Applications read the decrypted files at runtime.

Encrypt Secrets with age

services.myapp is an illustrative interface and must be defined by a real application module; the owner/group must also already exist. Pass the agenix input into the module through specialArgs or an equivalent mechanism.

{ config, pkgs, agenix, ... }:

{
  imports = [ agenix.nixosModules.default ];
  
  age.secrets.api-key = {
    file = ./secrets/api-key.age;
    owner = "myapp";
    group = "myapp";
  };
  
  services.myapp = {
    enable = true;
    apiKeyFile = config.age.secrets.api-key.path;
  };
}

Deployment Checks and Rollback#

# 先测试配置
nixos-rebuild test \
  --flake .#remote-server \
  --target-host deploy@remote-server --use-remote-sudo

# 如果出问题,远程回滚
ssh deploy@remote-server 'sudo nixos-rebuild switch --rollback'

# 查看远程系统版本
ssh deploy@remote-server 'sudo nix-env -p /nix/var/nix/profiles/system --list-generations'

Continuous Deployment with CI/CD#

GitHub Actions Example

This is an actual deployment workflow: a push triggers changes on the remote system. Production environments should use protected deployment environments/approvals and least privilege, with third-party actions pinned to verified commit SHAs. Installing Nix does not guarantee that the runner has nixos-rebuild; supply the appropriate version through a pinned tool environment. Prepopulate known_hosts after verifying it through an independent trusted channel. An ad hoc ssh-keyscan only collects candidate public keys; it does not establish trust in the server's identity.

name: Deploy NixOS

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Install Nix
        uses: DeterminateSystems/nix-installer-action@main
        
      - name: Setup SSH
        run: |
          mkdir -p ~/.ssh
          echo "$DEPLOY_KEY" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts
          chmod 600 ~/.ssh/known_hosts
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
          DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
          
      - name: Deploy
        run: |
          nixos-rebuild switch \
            --flake .#remote-server \
            --target-host deploy@remote-server --use-remote-sudo

References & further reading

  1. nix.dev: Deploying Configurations over SSH ↗
  2. Official deploy-rs Repository ↗
  3. deploy-rs CLI Argument Source ↗
  4. Official agenix Repository ↗

Adapted from technical notes. Examples use generic names.

Continue along a reading path

NixOS virtualization and deployment

  1. 01Validating NixOS configuration with QEMU
  2. 02Building a bootable NixOS ISO
  3. 03Building and running container images with dockerTools
  4. 04Deploying NixOS over SSH
  5. 05Dividing deployment responsibilities between Terraform and NixOS
  6. 06Designing layered desktop and development environments
CONTINUE READING / NEXT ARTICLEDividing deployment responsibilities between Terraform and NixOS

Manage cloud resources with Terraform, then deploy NixOS system configuration through a controlled process.