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.stateVersionspecifies 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 sshd2. 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.nixArguments:
--target-host: target server address--build-host: specifies a remote build host; when omitted, builds normally run locally. Do not assume that the special valuelocalhostbehaves 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-sudoDeploying 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.toplevelManaging 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-sudoReferences & further reading
- nix.dev: Deploying Configurations over SSH ↗
- Official deploy-rs Repository ↗
- deploy-rs CLI Argument Source ↗
- Official agenix Repository ↗
Adapted from technical notes. Examples use generic names.