quadlet-nix
NixOS module for Quadlet / podman-systemd. Inspired by the excellent work of SEIAROTg, but rewritten from scratch. You can get started with the following minimal configuration:
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
quadlet-nix = {
url = "github:mirkolenz/quadlet-nix/v1";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { nixpkgs, quadlet-nix, ... }: {
nixosConfigurations.default = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
quadlet-nix.nixosModules.default
({ pkgs, ... }: {
virtualisation.quadlet.enable = true;
virtualisation.quadlet.containers = {
hello-world = {
containerConfig.Image = "docker.io/library/hello-world:latest";
};
nginx = {
imageStream = pkgs.dockerTools.examples.nginxStream;
};
};
})
];
};
};
}
All available options are described in the documentation. You may also want to take a look at the tests for more examples.
Quoting values
For keys that hold KEY=VALUE assignments (e.g., Environment, Label, Annotation), use the attrset form so the entries are quoted automatically:
containerConfig.Environment = { TZ = "Europe/Berlin"; };
containerConfig.Label = { description = "My web server"; };
The same result can be expressed via the list form with lib.strings.toJSON, which is useful when an attrset cannot represent the value (e.g., duplicate keys):
containerConfig.Label = [ (lib.strings.toJSON "description=My web server") ];
All other values are written into the unit file verbatim.
Whitespace and other special characters are handled by Quadlet itself when it builds the resulting ExecStart= line, so no additional quoting is required.
Restart and rate-limit defaults
For long-running units (.container, .kube, .pod) the module sets a few [Service] / [Unit] keys as lib.mkDefault to replace systemd defaults that are unsafe for containers.
| Key | Default | Reason |
|---|---|---|
Restart | on-failure | Systemd’s no means containers don’t auto-restart at all. |
RestartSec | 5s | Systemd’s 100ms paired with Restart= produces millisecond-scale restart loops. |
TimeoutStartSec | 900s | Systemd’s 90s is often too short for image pulls or cold-start workloads. |
StartLimitBurst / StartLimitIntervalSec | 3 / 600s | Hard-fail after 3 restarts in 10 minutes so a broken unit doesn’t loop forever. |
The burst limit only fires when (TimeoutStartSec + RestartSec) × StartLimitBurst ≤ StartLimitIntervalSec, which holds for typical fast-starting services.
A unit that consistently hangs all the way to TimeoutStartSec will retry indefinitely because each attempt falls outside the rate-limit window.
Tighten TimeoutStartSec (or widen StartLimitIntervalSec) downstream when you want hung-start loops to hard-fail.
Overriding the generated unit
Each *Config section passes its keys through verbatim, so any Quadlet key can be set directly under its section using the upstream PascalCase name.
For the rare cases this does not cover, two escape hatches are available.
extraConfig is a freeform set of [Section] keys merged over the generated unit and takes precedence, so it can override a value the module sets itself:
# Any [Section], any value, written through with the last word:
extraConfig.Service.TimeoutStartSec = "300";
rawConfig replaces the entire unit file with the given text, bypassing generation completely:
rawConfig = ''
[Container]
Image=docker.io/library/hello-world:latest
'';
Comparison to SEIAROTg/quadlet-nix
The two implementations solve the same problem but make different trade-offs. The clearest difference is where unit correctness is checked: this version validates at build time, the original at evaluation time.
Where this version differs
- Unit files are produced inside a Nix derivation by invoking
podman-system-generator/podman-user-generatorat build time, rather than relying on the systemd generator at boot. The resulting package is added tosystemd.packages. The generator is built from the podman sources with its flag ordering made deterministic, so a regenerated unit only changes when its configuration does and nothing else is restarted on activation. - Quadlet keys are passed through verbatim using their original
PascalCasenames (e.g.,containerConfig.Image,containerConfig.PublishPort). The upstream Podman documentation applies directly and new Quadlet keys work without changes to this module. - Because the generator runs at build time, podman itself validates the units and the build aborts unless every expected unit is emitted. Errors the generator would otherwise log and skip at boot become hard build failures, checked by the same tool that consumes the units instead of by option types that have to be kept in sync with upstream.
- Rootless containers are supported directly from the NixOS module by setting a
uidper object, Home Manager is not required. - Container images can be supplied as Nix packages via
imageFile(e.g.,pkgs.dockerTools.buildImage) orimageStream(e.g.,pkgs.dockerTools.streamLayeredImage). - Releases follow semantic versioning with version tags (e.g.,
v1) for stable pinning and the flake is structured with flake-parts. - Long-running units (
.container,.kube,.pod) ship with overridable restart and rate-limit settings.
Where the original may suit you better
- Each Quadlet key is exposed as a dedicated, individually typed and documented option (e.g.,
containerConfig.publishPorts : listOf str), giving you per-field type checking and inline help. - That typing catches structural mistakes earlier, during evaluation rather than at build time, e.g., rejecting
Exec = [ "a" "b" ]since Quadlet only honors a single value. - Longer track record and a more elaborate README with recipes and comparisons to other tools.