April 20, 2020·6 min read·engineeringnixnixosupgradesservices

NixOS 20.03 makes service migration part of the release

Markhor updates the system base while documenting the state and permission transitions that a declarative rebuild cannot infer on its own.

A declarative operating-system configuration describes the system to build, but an existing service can carry state that requires an ordered transition. NixOS 20.03 makes that distinction visible in its release notes. Alongside a newer kernel and compiler, it documents database migration paths and changes to certificate handling. The resulting system configuration is only part of an upgrade's meaning when the service has persistent data of its own.

The April 20 release announcement introduces Markhor as the new stable release. The release notes preserved with 20.03 describe the package changes and module transitions. These are useful together because the announcement identifies the release while the notes explain what an existing configuration needs to express differently.

I am interested in NixOS because declarative configuration gives operational decisions a form that can be read and maintained. That preference does not require assuming every change to persistent state is reversible by selecting an earlier program. The migration details in this release are part of making the declared system correspond to what its services can actually run.

The system base and installation environment change together

The release moves the default Linux kernel branch from 4.19 to 5.4. Its notes list GCC 9.2.0 and glibc 2.30 among the core updates, with OpenSSL moving to 1.1.1d. These changes affect the base used to construct packages. They are distribution choices that accompany the service modules, rather than separate instructions for manually replacing individual system components.

Desktop updates include GNOME 3.34 and Plasma 5.17.5. The graphical installation image also starts its graphical session automatically. Previously a user reached a terminal and had to start the display manager explicitly. A boot-menu option can disable the display manager when that automatic behavior is unsuitable. This is a concrete change to the first interaction with the installation environment.

The notes also replace separate default desktop and window-manager settings with services.xserver.displayManager.defaultSession. The example combines Xfce and IceWM in one session identifier. That reflects the release's stated goal of improving support for upstream session files. A configuration therefore names the selected session directly instead of relying on two independent defaults to imply it.

services.xserver.displayManager.defaultSession = "xfce+icewm";

This setting replaces the older pair of default options in the release-note example. It is a configuration fragment for a system where the relevant desktop and window manager are configured. Selecting a default session does not itself establish that every component required by that session has been enabled.

Nextcloud needs an intermediate data upgrade

The Nextcloud notes demonstrate why declaring the newest package is not always a valid migration. The release includes Nextcloud 18.0.2, while the preceding NixOS stable release uses 16.0.8. Nextcloud permits advancing only one major version at a time according to the NixOS notes. A direct change between those package versions would therefore omit a required application transition.

The module addresses this by replacing an unversioned package attribute with versioned attributes for Nextcloud 17 and 18. The notes explain that existing configurations are detected through system.stateVersion, with Nextcloud 17 selected by default for the relevant upgrade path. A warning then recommends explicitly selecting the latest version after that deployment. The intermediate package is a migration step, rather than an arbitrary delay in adopting the release's newer software.

This design also makes the package choice visible through services.nextcloud.package. An explicit package option gives the configuration a place to record the intended application stage. It avoids allowing a changing unversioned alias to silently select a major version that the stored data cannot reach directly. The service's upgrade rules therefore influence the module's interface.

The notes identify an additional complication for overlays. An existing overlay can retain the former unversioned attribute, so the module raises an evaluation error by default in the described situation. That prevents accidental selection of an older version through assumptions the new module no longer makes. An explicit package setting can resolve the ambiguity, with responsibility for the chosen transition remaining visible in configuration.

Hydra separates schema preparation from activation

Hydra has a staged database change for improved indexing and identifiers. The notes require deploying an intermediate migration package that adds nullable columns, filling those columns using hydra-backfill-ids, and then deploying the newer package that uses the database changes. These are separate operations because adding a schema element and making existing rows ready for its use are different tasks.

The configuration example exposes the first stage directly:

{ pkgs, ... }: {
  services.hydra.package = pkgs.hydra-migration;
}

The backfill operates on existing data, so it is not equivalent to building this package. Its duration depends on database size according to the notes. Only after that work does switching to the newer Hydra package activate the intended database optimization. A system can have the right executable available while its stored data still lacks the preparation that executable expects.

Here stateVersion influences which migration package the module selects automatically. The notes explicitly warn that configurations selecting the newer stage without running the backfill can break the service. This gives the compatibility setting an operational role beyond identifying which NixOS release a machine currently follows. It helps preserve assumptions about how a service's state reached its present form.

My preference is for upgrade procedures to name intermediate states this clearly. A declarative package choice can represent each stage, while the data operation remains an explicit step with its own completion condition. That is more useful than an unqualified claim that a successful system build establishes application readiness.

Certificates retain their own transition rules

The ACME module switches from simp-le to lego. The notes connect the change to support for DNS-01 challenges and wildcard certificates. Configuration gains DNS-provider and credentials-file settings, while accepting the service terms and providing an email become explicit requirements. This is a change to both the certificate client and the module contract around it.

The transition regenerates certificates on activation. Accounts and certificates are not migrated from simp-le, and private keys are not preserved. The notes also explain that lego's default renewal behavior creates a new private key. An application that embeds a public key therefore has an assumption beyond the validity period of a particular certificate. A renewed certificate can be valid while that embedded expectation no longer matches.

The previous simp-le credentials remain available so that rollback does not inherently prevent the older client from generating certificates. This is a specific preservation decision documented by the module. It should not be generalized into a claim that every piece of external certificate state returns to its former value when the system configuration rolls back.

Service identity affects access to configuration

Nginx now starts entirely as a less-privileged user, rather than starting its master as root and reducing privileges only for workers. Its configured user and group must consequently be able to read the files needed at startup. The notes explicitly include configuration fragments, certificates, and keys in that requirement. Ownership of an unchanged file can therefore matter after the executable and service unit have been rebuilt successfully.

This illustrates a different migration boundary from the database examples. The service's persistent content need not change format, yet its access conditions change because the process identity changes. Configuration that names an external file also relies on that file's permissions. The service module cannot make an unreadable key usable merely by declaring its path correctly.

ZFS service defaults provide another example of configuration determining which maintenance work runs. The notes enable weekly trimming for supported devices and expose services.zfs.trim.enable as its control. Scrubbing and automatic snapshots are enabled only when ZFS appears among the supported filesystems in the relevant boot configuration. Configuring a ZFS mountpoint adds it to those lists automatically. This connects maintenance services to the system's declared filesystem use. It also means a service default should be understood together with the condition that activates it, instead of being read as an unconditional action on every NixOS installation. The distinction is visible in the release's explanation of the module behavior.

The release also moves the NixOS testing driver to Python and gives users of the Perl driver a transition period. That affects configurations using the integration-test framework, independently of runtime service state. Together these changes make Markhor an update to the system base and the procedures surrounding it. The release notes give maintainers concrete package stages, configuration replacements, and permission requirements for expressing the intended upgraded system accurately.