DEV Community

Cover image for NixOS on Servers: What Changes When Your OS Becomes Code
Pedro Tunin
Pedro Tunin

Posted on

NixOS on Servers: What Changes When Your OS Becomes Code

You know the kind of server.

It has been running for three years. The original setup came from a script, a few changes were made by hand during incidents, and the documentation stopped matching reality somewhere along the way. Everyone is slightly nervous about touching it. Rebuilding it from scratch would be an archaeological project.

The problem is not that the team forgot to automate everything. It is that the machine is partly defined by its history: every command, hotfix, and one-off decision that happened to it.

NixOS takes a different approach. Instead of describing the steps used to configure a machine, you describe the state you want. Services, users, packages, firewall rules, and many other system properties live in a declarative configuration.

NixOS is not just another way to automate shell commands. It turns the desired state of the operating system into something you can build, review, and version.

That idea is especially appealing on servers, where consistency matters and small configuration differences have a habit of becoming large production incidents. NixOS does not make operations effortless, but it does make much of the system less mysterious.

A quick map of the Nix ecosystem

Before going further, it helps to untangle four names that are often used interchangeably:

  • Nix is the package manager and build system. It stores results in isolated paths in the Nix store and aims to build software from explicitly described dependencies.
  • The Nix language is the functional language used to write expressions, packages, modules, and configurations.
  • Nixpkgs is the large collection of packages and modules maintained by the community.
  • NixOS is the Linux distribution that uses these components to build not only applications, but the operating system configuration as a whole.

You can use Nix on other Linux distributions and on macOS. NixOS goes further by applying the same model to the system itself: the kernel, services, users, networking, and much more.

From commands to desired state

On a traditional distribution, setting up a web server often looks something like this:

  1. install Nginx;
  2. edit files under /etc;
  3. create users;
  4. change firewall rules;
  5. enable and start services;
  6. document everything—if there is time.

There is nothing inherently wrong with those steps. The trouble is that their result depends on the machine's previous state and on every command that came before. Repeat, skip, or slightly change one step and two supposedly identical servers begin to drift apart.

With NixOS, the configuration declares properties:

services.nginx.enable = true;
services.openssh.enable = true;
networking.firewall.allowedTCPPorts = [ 22 80 443 ];
Enter fullscreen mode Exit fullscreen mode

NixOS evaluates the complete configuration and builds a new system generation from it. You can inspect and test that generation before deciding to activate it.

There is an important limit here. Databases, uploads, logs, and other persistent data do not magically become declarative. NixOS describes how the machine should be configured; mutable data still needs its own lifecycle, backup, and recovery plan.

Describing a web server in one file

Theory is useful, but a configuration file makes the model easier to see. The example below combines Nginx, key-based SSH access, a firewall, an administrative user, a few operational tools, and HTTPS certificates issued through ACME.

This is a compact demonstration, not a drop-in production template. Before trying it, replace:

  • server.example.com with a domain whose DNS points to the server;
  • infra@example.com with a valid email address for ACME notifications;
  • the value beginning with ssh-ed25519 with your complete SSH public key.
{ config, pkgs, ... }:

let
  site = pkgs.writeTextDir "index.html" ''
    <!doctype html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <title>Declarative server</title>
      </head>
      <body>
        <h1>This server was described with NixOS.</h1>
      </body>
    </html>
  '';
in
{
  networking.hostName = "web-01";
  time.timeZone = "UTC";

  environment.systemPackages = with pkgs; [
    curl
    git
    htop
  ];

  users.users.deploy = {
    isNormalUser = true;
    description = "Deployment user";
    extraGroups = [ "wheel" ];
    openssh.authorizedKeys.keys = [
      "ssh-ed25519 AAAA_REPLACE_WITH_YOUR_PUBLIC_KEY"
    ];
  };

  # Convenient for automation, but this decision should be reviewed
  # against your organization's privilege-management policy.
  security.sudo.wheelNeedsPassword = false;

  services.openssh = {
    enable = true;
    settings = {
      PasswordAuthentication = false;
      KbdInteractiveAuthentication = false;
      PermitRootLogin = "no";
    };
  };

  services.nginx = {
    enable = true;
    recommendedProxySettings = true;
    recommendedTlsSettings = true;

    virtualHosts."server.example.com" = {
      root = site;
      enableACME = true;
      forceSSL = true;
    };
  };

  security.acme = {
    acceptTerms = true;
    defaults.email = "infra@example.com";
  };

  networking.firewall = {
    enable = true;
    allowedTCPPorts = [ 22 80 443 ];
  };

  # Enable this only after defining an update strategy,
  # version source, and maintenance window.
  system.autoUpgrade = {
    enable = false;
    allowReboot = false;
  };

  # Use the version generated when your NixOS system was installed.
  system.stateVersion = "REPLACE_WITH_INSTALL_VERSION";
}
Enter fullscreen mode Exit fullscreen mode

The file leaves out hardware configuration, disk partitioning, monitoring, and plenty of other production concerns. Still, it tells us quite a lot: which services should exist, who can access the machine, which ports are open, and what Nginx should serve.

One line is easy to misunderstand: system.stateVersion. Despite the name, it is not the current NixOS release. It controls compatibility defaults for stateful services and should normally keep the value created during installation unless you deliberately plan a migration.

The example also disables password authentication over SSH. That means the public key must work before activation, or you can lock yourself out. Keep an existing administrative session open while testing and make sure you have another recovery path.

A safer rhythm for system changes

A change to configuration.nix does not need to go live immediately. NixOS gives us a useful progression.

First, build the configuration without activating it:

sudo nixos-rebuild build
Enter fullscreen mode Exit fullscreen mode

This evaluates and builds the new generation while leaving the running system unchanged.

Next, test it on the running system without making it the default for the next boot:

sudo nixos-rebuild test
Enter fullscreen mode Exit fullscreen mode

After validation, activate the generation and make it the default:

sudo nixos-rebuild switch
Enter fullscreen mode Exit fullscreen mode

If a configuration change causes problems, switch back to the previous generation:

sudo nixos-rebuild switch --rollback
Enter fullscreen mode Exit fullscreen mode

Previous generations may also appear in the boot menu, provided they have not been removed by garbage collection.

This makes system changes less intimidating, but it is not magic. During activation, NixOS still performs real work, including restarting affected services. A generation switch is not a promise of zero downtime.

There is an even more important distinction:

A NixOS rollback is not a database rollback.

If a new application version runs an incompatible database migration, returning to the previous system generation will not undo it. Backups, restore drills, reversible migrations, and database-aware deployment strategies are still essential.

Where this starts to pay off

Versioned and reviewable configuration

Once Nix files live in Git, infrastructure changes can follow a familiar process: pull requests, review, history, approvals, and automated checks.

Instead of a ticket that simply says “enable SSH,” reviewers can see whether password login is allowed, whether root can connect, and exactly which users receive access.

Less drift between servers

Modules make it possible to share policies across machines. An organization can define a common baseline for observability, NTP, SSH, internal certificates, and security agents while letting each host add only its own specifics.

Modules do not guarantee identical machines. Architecture, pinned inputs, secrets, external services, and persistent data all matter. They do, however, make a much larger part of the system explicit.

More predictable changes and recovery

Every rebuild creates a generation, and results in the Nix store are not overwritten in place. A team can prepare a configuration before activating it and keep earlier generations available for recovery.

That makes two awkward incident questions much easier to answer: “What changed?” and “What can we safely return to?”

Testing before production

NixOS configurations can be evaluated and built in CI. The ecosystem also supports integration tests using virtual machines, including multi-node scenarios and service-level assertions.

A green build is not the same as a successful production deployment. It can still catch invalid options, missing dependencies, and many compatibility problems before they reach a server.

Binary caches

Previously built results can be downloaded from binary caches instead of being rebuilt on every server. Organizations can also operate private caches for internal artifacts.

This is not only a performance feature. It separates building from activating: a production server can consume an artifact that was created and validated elsewhere.

Auditing with more context

Command logs show what someone attempted to execute. A versioned configuration shows which state the organization decided to maintain.

That distinction helps during audits and investigations. Of course, Git alone is not governance; access controls, protected branches, artifact signing, and pipeline audit trails still matter.

So, does it replace Ansible or Terraform?

Usually, no. These tools often operate at different layers.

Terraform commonly provisions resources such as instances, networks, disks, and cloud services. NixOS can define the operating system running inside one of those instances.

Ansible generally describes tasks applied to an existing machine. It can install packages, change files, and restart services. NixOS instead builds a system configuration from a declarative description.

NixOS can remove a surprising amount of imperative automation. Ansible may still be useful for orchestration and for systems that do not run NixOS.

Kubernetes addresses a different set of concerns, primarily the orchestration of containerized workloads. Even a Kubernetes cluster needs configured nodes, and NixOS can be one way to define them.

Trying to make one tool own every layer rarely ends well. A cleaner split might look like this:

  • the cloud platform provisions the machine;
  • the Nix configuration defines the operating system;
  • a deployment tool coordinates the fleet;
  • the application platform manages workloads.

A note about Flakes

Flakes provide a standardized input and output structure for Nix projects. A flake.lock file records dependency revisions, helping answer which version of Nixpkgs was used to build a particular configuration.

This matters because “declarative” does not automatically mean “reproducible.” If your package source changes over time, two builds of the same configuration can select different versions. Pinning the inputs closes much of that gap.

Flakes are widely used, but the official documentation still calls them experimental. That does not make them unusable; it simply belongs in the engineering decision. NixOS works without Flakes, and teams that use them can keep the flake layer thin while putting most logic in reusable modules.

There is also an essential security rule: files involved in a build may be copied to the Nix store, which is not an appropriate place for plaintext secrets.

The parts that are not so easy

This is where the enthusiasm needs a reality check. NixOS has real costs.

The learning curve is significant. Teams need to understand the Nix language, lazy evaluation, modules, attribute sets, and the distinction between building and activating a configuration.

Errors are not always easy to interpret. Evaluation messages can be long, especially when a problem crosses several modules.

The skill set is less common. A familiar distribution may be easier to hand over to a large team or an external provider.

Not every application fits the Nix model easily. Nixpkgs is extensive, but internal applications, proprietary binaries, and installers that expect to modify traditional system directories may require packaging work or adaptation.

Secrets need a separate strategy. Passwords, tokens, and private keys should not be written directly into files that will enter Git or the Nix store. Tools such as sops-nix and agenix are common options, but they introduce their own decisions around encryption, identity, and rotation.

A fleet requires more than nixos-rebuild. Multiple machines still need inventory, coordination, staged updates, observability, and recovery. Tools such as Colmena, deploy-rs, and nixos-anywhere can help, but they also need to be evaluated and operated.

Persistent state remains persistent state. NixOS can reproduce a PostgreSQL configuration; it cannot reproduce company data without backups and dedicated recovery procedures.

Is NixOS a good fit for your team?

It is worth a serious look when:

  • consistency across machines has high value;
  • configurations need to be reviewed and audited;
  • the team is willing to invest in learning;
  • testing system configuration before deployment is important;
  • system rollback and dependency isolation reduce meaningful risks;
  • the organization can retain internal knowledge about the platform.

It may be a poor fit when traditional vendor support is the priority, critical software depends heavily on imperative installers, or the team has no room to absorb a new language and operational model.

There is no need to make the company's most critical application the first experiment. Start with an internal server that holds no unique data: a static site, a disposable CI runner, or a development tool. Put its configuration in Git, build it in CI, rehearse an update, perform a rollback, and see how much of the environment you can actually reproduce.

If that pilot works, resist the urge to migrate everything at once. Turn what you learned into reusable modules, policies, and runbooks first.

Conclusion

What makes NixOS interesting is not a new syntax for installing packages. It is the idea that an operating system configuration can be built much like any other artifact.

That brings server management closer to practices we already trust in software: review, version control, testing, and traceable changes. In the right environment, it can reduce drift and make recovery far less improvised.

NixOS does not eliminate mutable data, replace backups, or operate an entire fleet for you. What it does is trade some hidden complexity for complexity you can see, discuss, and review.

Servers do not suddenly become simple. They do become a little less mysterious—and that is already a meaningful improvement.

Does your team already use Nix or NixOS in production? What benefits and challenges have you encountered? Share your experience in the comments.

References

Top comments (0)