NixOS 20.09 extends the contracts expressed by system modules
Nightingale adds desktop choices and updates Python while making container backends, option validation, and service access more explicit in configuration.
A system configuration can say which application should run while leaving important details about its execution implicit. NixOS 20.09 changes several of those details in the module system. Container declarations can select a backend, services receive tighter execution conditions, and option evaluation detects more invalid definitions. These changes make the release relevant to operators even when a newer desktop or interpreter is not their immediate reason to upgrade.
The October 27 announcement introduces Nightingale and describes the stabilization work behind it. Its release notes document both additions and incompatible changes. The distinction between those categories matters because a configuration can need adjustment to retain an existing service's behavior, independently of adopting something newly available.
I am interested in NixOS because a declared system gives operational choices a place to live in source. That advantage grows when the declaration includes how a service executes, rather than merely which package it installs. The module changes in this release make several such choices more explicit.
Container declarations acquire a backend boundary
The release renames declarative Docker containers from docker-containers to virtualisation.oci-containers.containers. Its stated purpose is to permit Podman as an alternative to Docker. A separate Podman module and a shared containers module accompany the change. The naming now describes the container declaration independently of the original execution engine.
The OCI containers module at the release tag exposes an image, command arguments, and environment variables as options. These are structured fields with declared types. The configuration therefore describes parts of a container invocation through the module interface instead of collecting all its meaning into an opaque command string.
An image can also come from an image file rather than a registry pull. The module documentation states that setting imageFile prevents pulling from the registry, while image remains required as the name used to start it. This distinguishes where the image content comes from and how the engine identifies it. One setting does not make the other's role disappear.
The module's port option accepts mappings that include a host address. That makes the listening interface part of the declaration where needed. Its default logging driver is journald, so logs integrate with the service's systemd unit. These concrete defaults and fields explain what the container abstraction actually does, beyond the broad statement that the system supports another engine.
Rootless operation receives another piece of integration. The release notes say subordinate UID and GID mappings are configured automatically for normal users, allowing tools such as Podman to work without root out of the box. That is an operating-system setup change alongside the package and module additions. Installing an executable alone would not express the account mappings it requires.
Freeform modules balance declared options with application settings
The module system gains freeform modules that combine aspects of types.attrsOf and types.submodule. The notes describe them as allowing an explicitly declared subset of options while still accepting definitions without corresponding options. This gives a module a way to validate settings it understands without requiring it to enumerate every setting accepted by the underlying application.
That is a meaningful design tension in declarative service management. A fully enumerated module can provide precise types and documentation, but it must keep pace with the application's settings. An unrestricted attribute set admits more configuration without providing those guarantees for every field. Freeform modules permit both within a defined interface, with the distinction between declared and undeclared settings remaining relevant.
The release also evaluates option definitions more strictly, which can reject configurations that previously evaluated. Acceptance by an earlier evaluator is consequently insufficient evidence that every definition followed the intended rules. This change affects evaluation before a system build or service activation begins. It gives the configuration author a different kind of failure from an application rejecting its generated configuration at startup.
The nix.buildMachines option gains type checking as well. The notes say there is no functional change, but configurations can need corrected attribute types. This illustrates why validating a declaration matters independently of changing its intended meaning. A field with the wrong type is a problem even if an earlier path through evaluation happened to tolerate it.
Service isolation changes which files are accessible
The MySQL service receives additional systemd isolation settings, including PrivateTmp, ProtectHome, and ProtectSystem. The release notes connect these settings to operations that import or export files outside the database's own state directory. A database query can be valid while the service's execution environment prevents the corresponding file access.
For an export directory that the service must write, the notes give a targeted declaration:
systemd.services.mysql.serviceConfig.ReadWritePaths = [ "/var/data" ];
This admits that directory through the service's systemd restrictions. The database account still needs filesystem permission to write there, as the notes specify. Declaring an allowed path does not manufacture access rights or ensure that the directory contains the expected content. The module contract and ordinary file ownership remain separate conditions.
The service startup script also stops running as root. Existing installations need an appropriate database superuser account for the dedicated operating-system identity. The notes distinguish MariaDB's unix_socket authentication plugin from MySQL's auth_socket. That specificity matters because similar service names do not make their authentication mechanisms interchangeable.
Transmission receives comparable execution restrictions. Its service's root-directory setting confines access to the configured download directory by default, with BindPaths documenting how to add another required directory. Its RPC listener also defaults to the local network interface. These are behavior changes an operator can encounter without changing the application's package selection.
My preference is for service access to be represented by narrow, reviewable configuration. An explicit writable directory explains an operational requirement more clearly than disabling every restriction to recover an older environment. The release notes supply the relevant setting and its remaining permission conditions for that case.
Desktop and language defaults also shape the declared system
Nightingale adds an official GNOME installation image and Cinnamon 4.6. GNOME moves to 3.36, while Plasma moves to 5.18.5. These changes broaden installation and desktop choices. They remain distinct from the default kernel branch, which stays on 5.4 in this release according to the notes.
Python 3 now selects 3.8 instead of 3.7. A default interpreter attribute is a package-set decision, so a configuration relying on that default receives a different version even if its own source does not change. PHP similarly defaults to 7.4. The release also provides a more explicit PHP extension selection interface through withExtensions, allowing configurations to request a smaller chosen set.
The default PHP package retains its extensions, with opcache added. The new customization mechanism therefore does not mean every existing PHP configuration suddenly loses extension support. It gives authors another way to specify their intended environment. Preserving that distinction avoids presenting a new optional interface as an unconditional reduction in the default package.
Test infrastructure follows the same move toward explicit assumptions
The Perl NixOS testing driver is removed after its deprecation in the preceding release. External tests using make-test.nix or testing.nix must move to the Python equivalents. This affects the tools used to validate configurations, separately from the runtime language packages selected for applications. A working service configuration does not establish that a project's old test entry point remains available.
The VM runner also deletes previous machine state at test startup unless instructed to retain it. A test that relies on persisted VM state now needs to request that behavior. This makes the test's starting condition consequential in the same way service file access is consequential. Both are assumptions about execution that can remain hidden until a default changes.
Build helpers also change their validation defaults. buildGoModule enables doCheck by default, and buildRustPackage runs its check phase in release mode. Package-specific assumptions about test execution therefore belong in the build definition too.
The release thus includes changes to the artifacts a system offers and the conditions under which they run. Container interfaces and module types describe intent more precisely, while service isolation and test-driver changes expose assumptions in existing configurations. Nightingale's practical effect depends on those specific declarations as well as its package versions. The release notes identify where a configuration must state an access requirement, select an application environment, or adopt the supported test interface to retain its intended behavior.