On October 19, we tagged deploy-rs 1.0.0. It is the first versioned release of the Nix deployment tool we announced in 2020, and the first one on crates.io. Alongside it, we published update-daemon, our tool that updates flake inputs across many repositories and opens pull requests with the changes.
For several years before that, deploy-rs got almost none of our attention. Over time, most of our engineering time went to client projects, and internal tools got less of it. deploy-rs kept working well enough for our own deployments, so the backlog grew quietly.
This year we went through the backlog. We sorted the open issues, reviewed and merged the pull requests with the most impact, fixed a few bugs ourselves, and cut a release. This post covers what changed in deploy-rs and in Nix since 2020, how we use deploy-rs at Serokell, and what we plan to work on next.
Version 1.0.0 collects six years of fixes into something you can pin
If you haven’t used it: deploy-rs deploys Nix profiles defined in a flake to remote machines. A profile can be a NixOS system, an application, or a user profile, and if a deployment fails or the target can’t confirm it, magic rollback restores the previous profile.
Until this year, deploy-rs had no versions, and the only way to use it was to follow master. Now there is a git tag you can pin in your flake inputs, and a changelog for future releases:
inputs.deploy-rs.url = "github:serokell/deploy-rs/v1.0.0";
You can also install the deploy and activate binaries from crates.io:
cargo install deploy-rs --locked
It needs Rust 1.85 or newer, and you still need Nix to deploy. For deployments, we still recommend taking the CLI from your flake input rather than from crates.io: the activate binary on your nodes is built from that input, and a CLI of a different version may not match it. The crate is mostly useful if you want deploy-rs as a Rust library. Note that versions 0.1.2 and earlier on crates.io belonged to a different project, and its author, alianse777, kindly handed the name over to us; 1.0.0 is the first release of ours.
Most of the changes this year came from external contributors. We reviewed them, fixed some up, and merged them. A few of them you are likely to notice right away.
Deploys with remoteBuild now build and push to several hosts in parallel (#271, by PhilTaken). Local builds still run one after another to avoid overloading your machine, and activation is still sequential; more on that below.
You can also deploy only part of a flake. Profiles, nodes and whole deployments can be put into groups, and --groups deploys only those (#365, by pippaye). The new --test flag works like nixos-rebuild test: it activates a configuration without making it the boot default (#353, by cole-h).
Magic rollback no longer undoes good deployments. Because of a race, a fast confirmation could be missed, and deploy-rs then rolled back a deployment that had already been confirmed. davidvornholt fixed it in #385.
The rest are smaller, and most of them make failures easier to deal with. When activation fails, deploy-rs now stops waiting right away instead of sitting out the timeout (#373, by paulbertin). A failed Nix command shows the command and its error output (#310). Progress bars can be turned off with --no-progress and turn off by themselves in CI logs or when you pipe output into nix-output-monitor (#391), and output from the target is marked with 📠 to set it apart from local output (#401). Flake checks run once per flake instead of once per target (#330, by weriomat), and profiles built from content-addressed derivations are supported (#375, by poly2it). The changelog and git history have the full list.
Features from earlier years that never got an announcement
Several bigger features landed between our 2020 post and this year without a word from us. Since 2021, you can deploy several nodes with one command, with nodes that succeeded rolled back by default if a later one fails (#80, by ysndr), and use deploy-rs as a Rust library (#104, by nrdxp). remoteBuild itself, which builds a profile on the target host using its substituters, arrived in 2022 (#175, by PhilTaken). In 2023, activate.darwin made deploying to macOS with nix-darwin work without a custom activation script (#213), and a fix kept user profile deploys working after newer Nix versions moved user profiles (#231). In 2024, n-hass added interactiveSudo, which asks for the sudo password when you deploy through a non-root SSH user and keeps magic rollback working (#257). The same year, we started testing every pull request by deploying to virtual machines in CI (#264).
Nix kept changing, and deploy-rs had to follow
deploy-rs is a thin layer over Nix commands, and every change in their output or behavior reaches it sooner or later. These are the changes since 2020 that matter most for deploy-rs users.
Flakes are still experimental. They first shipped in a stable release with Nix 2.4 in November 2021, and they still need experimental-features. RFC 136 plans to stabilize them, with no date set. Until then, Nix commands can change between versions, and deploy-rs has to keep up.
The clearest example is Nix 2.32 (October 2025). It changed the JSON describing derivations, which deploy-rs parses, and Nix 2.33 changed it again. Both times, deploys broke on the new version until contributors fixed them, in #346 and #359. This kind of breakage is the main reason deploy-rs needs a maintainer even when nobody asks for new features.
There is also more than one Nix now. Lix forked from Nix 2.18 and had its first release in July 2024, and Determinate Systems ships its own Nix distribution. deploy-rs doesn’t officially support Lix, but some users already run it: #394 came from a Lix warning.
On the NixOS side, two programs that deploy-rs sits next to were rewritten. switch-to-configuration, which activates a NixOS configuration, now has a Rust version that is the default since NixOS 24.11. deploy-rs works with it without changes, and our CI tests cover it. nixos-rebuild was rewritten in Python, and the new nixos-rebuild-ng is the default since NixOS 25.11. It can deploy a single NixOS machine over SSH with --target-host. For one machine with one system configuration, that may be all you need. deploy-rs still adds deploying several profiles and nodes at once, user profiles, and magic rollback.
Finally, Nix 2.35, released in July 2026, no longer always copies a flake’s source to the store before evaluating it. deploy-rs users with large repositories may see faster evaluation, though we haven’t measured it.
We deploy almost all of our own infrastructure with it
Even while we weren’t maintaining deploy-rs, we kept depending on it. Our servers are split into clusters, each in its own repository, and all of them are deployed with deploy-rs. So are our company website and several internal tools.
Some of these repositories are public:
- gemini-infra, the cluster for our internal services.
- hackage-search, which lets you search across all of Hackage and runs at hackage-search.serokell.io.
- website-deployment-example, the example from our post Website deployment for Runtime Verification.
The infra template in nix-templates also comes with deploy-rs already set up.
Servers themselves we deploy the usual way, as a NixOS system profile. For the services running on them, we ended up with two approaches, and the difference between them is the one real lesson we have to share.
In the first, the app repository defines a NixOS module with a systemd service, and the infra repository imports that module as a flake input. The service is deployed as part of the server’s system profile, while CI deploys only the app binary, as a separate profile. Its activation script restarts the service, which then picks up the new binary. tzbot works this way, deployed to a server in gemini-infra. The downside is that any change to the module, such as a new environment variable, means updating the flake lock in the infra repository and redeploying the whole server.
In the second, the app repository deploys the whole service. It builds the systemd unit from a NixOS module, and CI deploys the unit’s closure with deploy-rs as a user-level service. The activation script links the unit into the systemd directory and restarts it. serokell.nix has a helper that builds this activation script, and the app repository wraps it in a deploy-rs profile. Here a settings change doesn’t touch the server configuration at all. website-deployment-example is a public example, and our Runtime Verification post walks through the setup in detail.
The second approach avoids the server redeploy, at the cost of more setup in each app repository. deploy-rs supports both because it can deploy any Nix profile, not only a NixOS system.
We’ll keep reviewing and merging, without promising dates
At the time of writing, deploy-rs has 40 open pull requests and 95 open issues, and we won’t clear that backlog quickly. I’m setting aside time every week to review pull requests and triage new issues. How much more we can do depends on our client work, so we’re not putting dates on features.
There are four problems we’d like to work on next. We picked them by how often people asked for them and how much work they look like.
The most requested feature is parallel deploys (#46). Builds with remoteBuild already run in parallel, but activation still goes one profile at a time. Deploying a NixOS configuration from a Mac, or the other way around, is harder than it sounds (#216): flake checks run locally even with --remote-build, and running them remotely would add a lot of complexity. Until then, --remote-build --skip-checks works around it.
Two smaller ones would make everyday deploys less annoying. deploy-rs opens several SSH connections per deploy, and if your key needs a PIN or passphrase, you type it again and again (#225). I’ve run into this one myself. It would also help to see a diff between the old and the new configuration (#356).
If one of these matters to you, or something else is missing, say so in the issue tracker. Pull requests are welcome too; small, focused ones are the quickest for us to review.
.png)
