Manages Podman containers and networks on NixOS via Quadlet.
Nix
395
104 commits
updated Aug 2, 2026
Manages Podman containers, networks, pods, etc. on NixOS via Quadlet.
This project was started in Aug 2023, as a result of the author's frustration on some relatively simple container management needs, where then available technologies are either overly restrictive, or overly complex that requires non-trivial but pointless investment ad-hoc domain knowledge.
quadlet-nix is designed to be a simple tool that just works. Quadlet options are directly mapped into Nix, allowing users to effectively manage their Podman resources in the Nix language, without having to acquire domain knowledge in yet another tool. Prior knowledge and documentation of Podman continue to apply.
Below are comparisons with several alternatives for declaratively managing Podman containers on NixOS, effective as of May 2025.
virtualisation.oci-containersarionservices.podmancompose2nixSee seiarotg.github.io/quadlet-nix for all options.
flake.nix{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
quadlet-nix.url = "github:SEIAROTg/quadlet-nix";
};
outputs = { nixpkgs, quadlet-nix, ... }@attrs: {
nixosConfigurations.machine = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./configuration.nix
quadlet-nix.nixosModules.quadlet
];
};
};
}
configuration.nix{ config, ... }: {
# ...
virtualisation.quadlet = let
inherit (config.virtualisation.quadlet) networks pods;
in {
containers = {
nginx.containerConfig.image = "docker.io/library/nginx:latest";
nginx.containerConfig.networks = [ "podman" networks.internal.ref ];
nginx.containerConfig.pod = pods.foo.ref;
nginx.serviceConfig.TimeoutStartSec = "60";
};
networks = {
internal.networkConfig.subnets = [ "10.0.123.1/24" ];
};
pods = {
foo = { };
};
};
}
flake.nix{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
home-manager.url = "github:nix-community/home-manager";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
quadlet-nix.url = "github:SEIAROTg/quadlet-nix";
};
outputs = { nixpkgs, quadlet-nix, home-manager, ... }@attrs: {
nixosConfigurations.machine = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./configuration.nix
home-manager.nixosModules.home-manager
# to enable podman & podman systemd generator
quadlet-nix.nixosModules.quadlet
];
};
};
}
configuration.nix{
# ...
# to enable podman & podman systemd generator
virtualisation.quadlet.enable = true;
users.users.alice = {
# ...
# required for auto start before user login
linger = true;
# required for rootless container with multiple users
autoSubUidGidRange = true;
};
home-manager.users.alice = { pkgs, config, ... }: {
# ...
imports = [ inputs.quadlet-nix.homeManagerModules.quadlet ];
virtualisation.quadlet.containers = {
echo-server = {
autoStart = true;
serviceConfig = {
RestartSec = "10";
Restart = "always";
};
containerConfig = {
image = "docker.io/mendhak/http-https-echo:31";
publishPorts = [ "127.0.0.1:8080:8080" ];
userns = "keep-id";
};
};
};
};
}
β οΈ Not officially supported by Podman. Use at your own risk and expect breaking changes.
{ config, ... }: {
users.users.alice = {
uid = 1234;
# required for auto start before user login
linger = true;
# required for rootless container with multiple users
autoSubUidGidRange = true;
};
virtualisation.quadlet.containers.nginx = {
rootlessConfig.uid = config.users.users.alice.uid;
containerConfig.image = "docker.io/library/nginx:latest";
};
}
{ config, ... }: {
# ...
virtualisation.quadlet = let
inherit (config.virtualisation.quadlet) volumes;
in {
containers.nginx.containerConfig.image = "docker.io/library/nginx:latest";
containers.nginx.containerConfig.volumes = [
"${volumes.nginx-config.ref}:/etc/nginx"
];
volumes.nginx-config.volumeConfig = {
type = "bind";
device = "/path/to/host/directory";
};
};
}
Containerfile){ pkgs, config, ... }: {
# ...
virtualisation.quadlet = let
inherit (config.virtualisation.quadlet) builds;
containerfile = pkgs.writeText "Containerfile" ''
FROM docker.io/library/nginx:latest
# ...
'';
in {
containers.nginx.containerConfig.image = builds.nginx.ref;
builds.nginx.buildConfig.file = containerfile.outPath;
};
}
{ config, ... }: {
# ...
virtualisation.quadlet = let
inherit (config.virtualisation.quadlet) builds;
src = builtins.fetchGit {
url = "https://github.com/alpinelinux/docker-alpine.git";
rev = "4dc13cbc7caffe03c98aa99f28e27c2fb6f7e74d";
};
in {
containers.example.containerConfig = {
image = builds.alpine.ref;
entrypoint = "/bin/sh";
exec = "-c 'echo 123'";
};
containers.example.serviceConfig.RemainAfterExit = true;
builds.alpine.buildConfig = {
tag = "alpine:3.22";
workdir = "${src}/x86_64";
};
};
}
Alternatively, git integration of Podman can be used through workdir = "https://github.com/nginx/docker-nginx.git". However, it will be users' responsibility to make binaries such as git available to the build service via PATH.
{ config, ... }: {
# ...
virtualisation.quadlet = let
inherit (config.virtualisation.quadlet) images;
in {
containers.nginx.containerConfig.image = images.nginx.ref;
images.nginx.imageConfig.image = "docker-archive:/path/to/local/image";
images.nginx.imageConfig.tag = "docker.com/library/nginx:latest";
};
}
If you wish to write raw Quadlet files instead of using the Nix options, you may do so with rawConfig. Using this will cause all other options (except autoStart) to be ignored though.
{ config, ... }: {
# ...
virtualisation.quadlet = let
inherit (config.virtualisation.quadlet) networks pods;
in {
containers = {
nginx.rawConfig = ''
[Container]
Image=docker.io/library/nginx:latest
Network=podman
Network=${networks.internal.ref}
Pod=${pods.foo.ref}
[Service]
TimeoutStartSec=60
'';
};
networks = {
internal.networkConfig.subnets = [ "10.0.123.1/24" ];
};
pods = {
foo = { };
};
};
}
pkgs.dockerToolsPodman natively supports multiple transport, including docker-archive that can be used with pkgs.dockerTools.
{ pkgs, ... }: let
image = pkgs.dockerTools.buildImage {
# ...
};
in {
virtualisation.quadlet.containers = {
foo.containerConfig.image = "docker-archive:${image}";
};
}
See: https://docs.podman.io/en/v5.5.0/markdown/podman-run.1.html#image
To use Podman DNS, it needs to be enabled and allowed by your firewall.
For the default network, below sets up both for you:
virtualisation.podman.defaultNetwork.settings.dns_enabled = true;
Or if you manage firewall separately, allow UDP port 53 on the input chain on host interface "podman0" and set:
virtualisation.podman.defaultNetwork.settings.dns_enabled = true;
virtualisation.podman.defaultNetwork.settings.network_interface = "podman0";
For custom networks managed by Quadlet, Podman DNS is enabled by default, unless disableDns is set. To set up the firewall rules:
virtualisation.quadlet.networks.foo.networkConfig.interfaceName = "br-foo";
networking.firewall.interfaces.br-foo.allowedUDPPorts = [ 53 ];
To apply this on all custom networks:
enable-dns.nix{ config, lib, ... }: {
options.virtualisation.quadlet.networks = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule ( { name, ... }: {
networkConfig.driver = "bridge";
networkConfig.interfaceName = "br-${name}";
}));
};
config.networking.firewall.interfaces = lib.mapAttrs' (name: _: {
name = "br-${name}";
value.allowedUDPPorts = [ 53 ];
}) config.virtualisation.quadlet.networks;
}
configuration.nix{
imports = [
./enable-dns.nix
];
# ...
}
Obvious dependencies such as those between containers and their networks are automatically set up by Quadlet, and thus no additional configuration is needed.
Extra dependencies can be set up in systemd unit config. Note that .ref syntax is only valid in quadlet and does not work from regular systemd units.
{ config, ... }: {
# ...
virtualisation.quadlet = let
inherit (config.virtualisation.quadlet) containers;
in {
containers = {
database = {
# ...
};
server = {
# ...
unitConfig.Requires = [ containers.database.ref "network-online.target" ];
unitConfig.After = [ containers.database.ref "network-online.target" ];
};
};
};
}
quadlet-nix tries to put containers into full management under systemd. This means once a container crashes, it will be fully deleted and debugging mechanisms like podman ps -a or podman logs will not work.
However, status and logs are still accessible through systemd, namely, systemctl status <service name> and journalctl -u <service name>, where <service name> is container name, <network name>-network, <pod name>-pod, or similar. These names are the names as appeared in virtualisation.quadlet.containers.<container name>, rather than podman container name, in case it's different.
Check if that option is supported by Podman Quadlet here: https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html.
If it exists, please create an issue or send a PR to add.
Otherwise, please use PodmanArgs and GlobalArgs to insert additional command line arguments as quadlet-nix does not intend to support options beyond what Quadlet offers.
Nix
100.0%
Manages Podman containers and networks on NixOS via Quadlet.
Nix
395
104 commits
updated Aug 2, 2026
Manages Podman containers, networks, pods, etc. on NixOS via Quadlet.
This project was started in Aug 2023, as a result of the author's frustration on some relatively simple container management needs, where then available technologies are either overly restrictive, or overly complex that requires non-trivial but pointless investment ad-hoc domain knowledge.
quadlet-nix is designed to be a simple tool that just works. Quadlet options are directly mapped into Nix, allowing users to effectively manage their Podman resources in the Nix language, without having to acquire domain knowledge in yet another tool. Prior knowledge and documentation of Podman continue to apply.
Below are comparisons with several alternatives for declaratively managing Podman containers on NixOS, effective as of May 2025.
virtualisation.oci-containersarionservices.podmancompose2nixSee seiarotg.github.io/quadlet-nix for all options.
flake.nix{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
quadlet-nix.url = "github:SEIAROTg/quadlet-nix";
};
outputs = { nixpkgs, quadlet-nix, ... }@attrs: {
nixosConfigurations.machine = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./configuration.nix
quadlet-nix.nixosModules.quadlet
];
};
};
}
configuration.nix{ config, ... }: {
# ...
virtualisation.quadlet = let
inherit (config.virtualisation.quadlet) networks pods;
in {
containers = {
nginx.containerConfig.image = "docker.io/library/nginx:latest";
nginx.containerConfig.networks = [ "podman" networks.internal.ref ];
nginx.containerConfig.pod = pods.foo.ref;
nginx.serviceConfig.TimeoutStartSec = "60";
};
networks = {
internal.networkConfig.subnets = [ "10.0.123.1/24" ];
};
pods = {
foo = { };
};
};
}
flake.nix{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
home-manager.url = "github:nix-community/home-manager";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
quadlet-nix.url = "github:SEIAROTg/quadlet-nix";
};
outputs = { nixpkgs, quadlet-nix, home-manager, ... }@attrs: {
nixosConfigurations.machine = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./configuration.nix
home-manager.nixosModules.home-manager
# to enable podman & podman systemd generator
quadlet-nix.nixosModules.quadlet
];
};
};
}
configuration.nix{
# ...
# to enable podman & podman systemd generator
virtualisation.quadlet.enable = true;
users.users.alice = {
# ...
# required for auto start before user login
linger = true;
# required for rootless container with multiple users
autoSubUidGidRange = true;
};
home-manager.users.alice = { pkgs, config, ... }: {
# ...
imports = [ inputs.quadlet-nix.homeManagerModules.quadlet ];
virtualisation.quadlet.containers = {
echo-server = {
autoStart = true;
serviceConfig = {
RestartSec = "10";
Restart = "always";
};
containerConfig = {
image = "docker.io/mendhak/http-https-echo:31";
publishPorts = [ "127.0.0.1:8080:8080" ];
userns = "keep-id";
};
};
};
};
}
β οΈ Not officially supported by Podman. Use at your own risk and expect breaking changes.
{ config, ... }: {
users.users.alice = {
uid = 1234;
# required for auto start before user login
linger = true;
# required for rootless container with multiple users
autoSubUidGidRange = true;
};
virtualisation.quadlet.containers.nginx = {
rootlessConfig.uid = config.users.users.alice.uid;
containerConfig.image = "docker.io/library/nginx:latest";
};
}
{ config, ... }: {
# ...
virtualisation.quadlet = let
inherit (config.virtualisation.quadlet) volumes;
in {
containers.nginx.containerConfig.image = "docker.io/library/nginx:latest";
containers.nginx.containerConfig.volumes = [
"${volumes.nginx-config.ref}:/etc/nginx"
];
volumes.nginx-config.volumeConfig = {
type = "bind";
device = "/path/to/host/directory";
};
};
}
Containerfile){ pkgs, config, ... }: {
# ...
virtualisation.quadlet = let
inherit (config.virtualisation.quadlet) builds;
containerfile = pkgs.writeText "Containerfile" ''
FROM docker.io/library/nginx:latest
# ...
'';
in {
containers.nginx.containerConfig.image = builds.nginx.ref;
builds.nginx.buildConfig.file = containerfile.outPath;
};
}
{ config, ... }: {
# ...
virtualisation.quadlet = let
inherit (config.virtualisation.quadlet) builds;
src = builtins.fetchGit {
url = "https://github.com/alpinelinux/docker-alpine.git";
rev = "4dc13cbc7caffe03c98aa99f28e27c2fb6f7e74d";
};
in {
containers.example.containerConfig = {
image = builds.alpine.ref;
entrypoint = "/bin/sh";
exec = "-c 'echo 123'";
};
containers.example.serviceConfig.RemainAfterExit = true;
builds.alpine.buildConfig = {
tag = "alpine:3.22";
workdir = "${src}/x86_64";
};
};
}
Alternatively, git integration of Podman can be used through workdir = "https://github.com/nginx/docker-nginx.git". However, it will be users' responsibility to make binaries such as git available to the build service via PATH.
{ config, ... }: {
# ...
virtualisation.quadlet = let
inherit (config.virtualisation.quadlet) images;
in {
containers.nginx.containerConfig.image = images.nginx.ref;
images.nginx.imageConfig.image = "docker-archive:/path/to/local/image";
images.nginx.imageConfig.tag = "docker.com/library/nginx:latest";
};
}
If you wish to write raw Quadlet files instead of using the Nix options, you may do so with rawConfig. Using this will cause all other options (except autoStart) to be ignored though.
{ config, ... }: {
# ...
virtualisation.quadlet = let
inherit (config.virtualisation.quadlet) networks pods;
in {
containers = {
nginx.rawConfig = ''
[Container]
Image=docker.io/library/nginx:latest
Network=podman
Network=${networks.internal.ref}
Pod=${pods.foo.ref}
[Service]
TimeoutStartSec=60
'';
};
networks = {
internal.networkConfig.subnets = [ "10.0.123.1/24" ];
};
pods = {
foo = { };
};
};
}
pkgs.dockerToolsPodman natively supports multiple transport, including docker-archive that can be used with pkgs.dockerTools.
{ pkgs, ... }: let
image = pkgs.dockerTools.buildImage {
# ...
};
in {
virtualisation.quadlet.containers = {
foo.containerConfig.image = "docker-archive:${image}";
};
}
See: https://docs.podman.io/en/v5.5.0/markdown/podman-run.1.html#image
To use Podman DNS, it needs to be enabled and allowed by your firewall.
For the default network, below sets up both for you:
virtualisation.podman.defaultNetwork.settings.dns_enabled = true;
Or if you manage firewall separately, allow UDP port 53 on the input chain on host interface "podman0" and set:
virtualisation.podman.defaultNetwork.settings.dns_enabled = true;
virtualisation.podman.defaultNetwork.settings.network_interface = "podman0";
For custom networks managed by Quadlet, Podman DNS is enabled by default, unless disableDns is set. To set up the firewall rules:
virtualisation.quadlet.networks.foo.networkConfig.interfaceName = "br-foo";
networking.firewall.interfaces.br-foo.allowedUDPPorts = [ 53 ];
To apply this on all custom networks:
enable-dns.nix{ config, lib, ... }: {
options.virtualisation.quadlet.networks = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule ( { name, ... }: {
networkConfig.driver = "bridge";
networkConfig.interfaceName = "br-${name}";
}));
};
config.networking.firewall.interfaces = lib.mapAttrs' (name: _: {
name = "br-${name}";
value.allowedUDPPorts = [ 53 ];
}) config.virtualisation.quadlet.networks;
}
configuration.nix{
imports = [
./enable-dns.nix
];
# ...
}
Obvious dependencies such as those between containers and their networks are automatically set up by Quadlet, and thus no additional configuration is needed.
Extra dependencies can be set up in systemd unit config. Note that .ref syntax is only valid in quadlet and does not work from regular systemd units.
{ config, ... }: {
# ...
virtualisation.quadlet = let
inherit (config.virtualisation.quadlet) containers;
in {
containers = {
database = {
# ...
};
server = {
# ...
unitConfig.Requires = [ containers.database.ref "network-online.target" ];
unitConfig.After = [ containers.database.ref "network-online.target" ];
};
};
};
}
quadlet-nix tries to put containers into full management under systemd. This means once a container crashes, it will be fully deleted and debugging mechanisms like podman ps -a or podman logs will not work.
However, status and logs are still accessible through systemd, namely, systemctl status <service name> and journalctl -u <service name>, where <service name> is container name, <network name>-network, <pod name>-pod, or similar. These names are the names as appeared in virtualisation.quadlet.containers.<container name>, rather than podman container name, in case it's different.
Check if that option is supported by Podman Quadlet here: https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html.
If it exists, please create an issue or send a PR to add.
Otherwise, please use PodmanArgs and GlobalArgs to insert additional command line arguments as quadlet-nix does not intend to support options beyond what Quadlet offers.
Nix
100.0%