dxvk_1: fix build compatibility with GCC 14 (#360918)
[NixPkgs.git] / nixos / modules / virtualisation / nixos-containers.nix
blobfe0fca58c440fc321431bf001b4f57f6d60b17e7
1 { config, lib, pkgs, ... }@host:
3 with lib;
5 let
7   configurationPrefix = optionalString (versionAtLeast config.system.stateVersion "22.05") "nixos-";
8   configurationDirectoryName = "${configurationPrefix}containers";
9   configurationDirectory = "/etc/${configurationDirectoryName}";
10   stateDirectory = "/var/lib/${configurationPrefix}containers";
12   nixos-container = pkgs.nixos-container.override {
13     inherit stateDirectory configurationDirectory;
14   };
16   # The container's init script, a small wrapper around the regular
17   # NixOS stage-2 init script.
18   containerInit = (cfg:
19     let
20       renderExtraVeth = (name: cfg:
21         ''
22         echo "Bringing ${name} up"
23         ip link set dev ${name} up
24         ${optionalString (cfg.localAddress != null) ''
25           echo "Setting ip for ${name}"
26           ip addr add ${cfg.localAddress} dev ${name}
27         ''}
28         ${optionalString (cfg.localAddress6 != null) ''
29           echo "Setting ip6 for ${name}"
30           ip -6 addr add ${cfg.localAddress6} dev ${name}
31         ''}
32         ${optionalString (cfg.hostAddress != null) ''
33           echo "Setting route to host for ${name}"
34           ip route add ${cfg.hostAddress} dev ${name}
35         ''}
36         ${optionalString (cfg.hostAddress6 != null) ''
37           echo "Setting route6 to host for ${name}"
38           ip -6 route add ${cfg.hostAddress6} dev ${name}
39         ''}
40         ''
41         );
42     in
43       pkgs.writeScript "container-init"
44       ''
45         #! ${pkgs.runtimeShell} -e
47         # Exit early if we're asked to shut down.
48         trap "exit 0" SIGRTMIN+3
50         # Initialise the container side of the veth pair.
51         if [ -n "$HOST_ADDRESS" ]   || [ -n "$HOST_ADDRESS6" ]  ||
52            [ -n "$LOCAL_ADDRESS" ]  || [ -n "$LOCAL_ADDRESS6" ] ||
53            [ -n "$HOST_BRIDGE" ]; then
54           ip link set host0 name eth0
55           ip link set dev eth0 up
57           if [ -n "$LOCAL_ADDRESS" ]; then
58             ip addr add $LOCAL_ADDRESS dev eth0
59           fi
60           if [ -n "$LOCAL_ADDRESS6" ]; then
61             ip -6 addr add $LOCAL_ADDRESS6 dev eth0
62           fi
63           if [ -n "$HOST_ADDRESS" ]; then
64             ip route add $HOST_ADDRESS dev eth0
65             ip route add default via $HOST_ADDRESS
66           fi
67           if [ -n "$HOST_ADDRESS6" ]; then
68             ip -6 route add $HOST_ADDRESS6 dev eth0
69             ip -6 route add default via $HOST_ADDRESS6
70           fi
71         fi
73         ${concatStringsSep "\n" (mapAttrsToList renderExtraVeth cfg.extraVeths)}
75         # Start the regular stage 2 script.
76         # We source instead of exec to not lose an early stop signal, which is
77         # also the only _reliable_ shutdown signal we have since early stop
78         # does not execute ExecStop* commands.
79         set +e
80         . "$1"
81       ''
82     );
84   nspawnExtraVethArgs = (name: cfg: "--network-veth-extra=${name}");
86   startScript = cfg:
87     ''
88       # Declare root explicitly to avoid shellcheck warnings, it comes from the env
89       declare root
91       mkdir -p "$root/etc" "$root/var/lib"
92       chmod 0755 "$root/etc" "$root/var/lib"
93       mkdir -p "$root/var/lib/private" "$root/root" /run/nixos-containers
94       chmod 0700 "$root/var/lib/private" "$root/root" /run/nixos-containers
95       if ! [ -e "$root/etc/os-release" ]; then
96         touch "$root/etc/os-release"
97       fi
99       if ! [ -e "$root/etc/machine-id" ]; then
100         touch "$root/etc/machine-id"
101       fi
103       mkdir -p \
104         "/nix/var/nix/profiles/per-container/$INSTANCE" \
105         "/nix/var/nix/gcroots/per-container/$INSTANCE"
106       chmod 0755 \
107         "/nix/var/nix/profiles/per-container/$INSTANCE" \
108         "/nix/var/nix/gcroots/per-container/$INSTANCE"
110       cp --remove-destination /etc/resolv.conf "$root/etc/resolv.conf"
112       declare -a extraFlags
114       if [ "$PRIVATE_NETWORK" = 1 ]; then
115         extraFlags+=("--private-network")
116       fi
118       if [ -n "$HOST_ADDRESS" ]  || [ -n "$LOCAL_ADDRESS" ] ||
119          [ -n "$HOST_ADDRESS6" ] || [ -n "$LOCAL_ADDRESS6" ]; then
120         extraFlags+=("--network-veth")
121       fi
123       if [ -n "$HOST_PORT" ]; then
124         OIFS=$IFS
125         IFS=","
126         for i in $HOST_PORT
127         do
128             extraFlags+=("--port=$i")
129         done
130         IFS=$OIFS
131       fi
133       if [ -n "$HOST_BRIDGE" ]; then
134         extraFlags+=("--network-bridge=$HOST_BRIDGE")
135       fi
137       if [ -n "$NETWORK_NAMESPACE_PATH" ]; then
138         extraFlags+=("--network-namespace-path=$NETWORK_NAMESPACE_PATH")
139       fi
141       extraFlags+=(${lib.escapeShellArgs (mapAttrsToList nspawnExtraVethArgs cfg.extraVeths)})
143       for iface in $INTERFACES; do
144         extraFlags+=("--network-interface=$iface")
145       done
147       for iface in $MACVLANS; do
148         extraFlags+=("--network-macvlan=$iface")
149       done
151       # If the host is 64-bit and the container is 32-bit, add a
152       # --personality flag.
153       ${optionalString (pkgs.stdenv.hostPlatform.system == "x86_64-linux") ''
154         if [ "$(< "''${SYSTEM_PATH:-/nix/var/nix/profiles/per-container/$INSTANCE/system}/system")" = i686-linux ]; then
155           extraFlags+=("--personality=x86")
156         fi
157       ''}
159       export SYSTEMD_NSPAWN_UNIFIED_HIERARCHY=1
161       # Run systemd-nspawn without startup notification (we'll
162       # wait for the container systemd to signal readiness)
163       # Kill signal handling means systemd-nspawn will pass a system-halt signal
164       # to the container systemd when it receives SIGTERM for container shutdown;
165       # containerInit and stage2 have to handle this as well.
166       # TODO: fix shellcheck issue properly
167       # shellcheck disable=SC2086
168       exec ${config.systemd.package}/bin/systemd-nspawn \
169         --keep-unit \
170         -M "$INSTANCE" -D "$root" "''${extraFlags[@]}" \
171         $EXTRA_NSPAWN_FLAGS \
172         --notify-ready=yes \
173         --kill-signal=SIGRTMIN+3 \
174         --bind-ro=/nix/store \
175         --bind-ro=/nix/var/nix/db \
176         --bind-ro=/nix/var/nix/daemon-socket \
177         --bind="/nix/var/nix/profiles/per-container/$INSTANCE:/nix/var/nix/profiles" \
178         --bind="/nix/var/nix/gcroots/per-container/$INSTANCE:/nix/var/nix/gcroots" \
179         ${optionalString (!cfg.ephemeral) "--link-journal=try-guest"} \
180         --setenv PRIVATE_NETWORK="$PRIVATE_NETWORK" \
181         --setenv HOST_BRIDGE="$HOST_BRIDGE" \
182         --setenv HOST_ADDRESS="$HOST_ADDRESS" \
183         --setenv LOCAL_ADDRESS="$LOCAL_ADDRESS" \
184         --setenv HOST_ADDRESS6="$HOST_ADDRESS6" \
185         --setenv LOCAL_ADDRESS6="$LOCAL_ADDRESS6" \
186         --setenv HOST_PORT="$HOST_PORT" \
187         --setenv PATH="$PATH" \
188         ${optionalString cfg.ephemeral "--ephemeral"} \
189         ${optionalString (cfg.additionalCapabilities != null && cfg.additionalCapabilities != [])
190           ''--capability="${concatStringsSep "," cfg.additionalCapabilities}"''
191         } \
192         ${optionalString (cfg.tmpfs != null && cfg.tmpfs != [])
193           ''--tmpfs=${concatStringsSep " --tmpfs=" cfg.tmpfs}''
194         } \
195         ${containerInit cfg} "''${SYSTEM_PATH:-/nix/var/nix/profiles/system}/init"
196     '';
198   preStartScript = cfg:
199     ''
200       # Clean up existing machined registration and interfaces.
201       machinectl terminate "$INSTANCE" 2> /dev/null || true
203       if [ -n "$HOST_ADDRESS" ]  || [ -n "$LOCAL_ADDRESS" ] ||
204          [ -n "$HOST_ADDRESS6" ] || [ -n "$LOCAL_ADDRESS6" ]; then
205         ip link del dev "ve-$INSTANCE" 2> /dev/null || true
206         ip link del dev "vb-$INSTANCE" 2> /dev/null || true
207       fi
209       ${concatStringsSep "\n" (
210         mapAttrsToList (name: cfg:
211           "ip link del dev ${name} 2> /dev/null || true "
212         ) cfg.extraVeths
213       )}
214    '';
216   postStartScript = (cfg:
217     let
218       ipcall = cfg: ipcmd: variable: attribute:
219         if cfg.${attribute} == null then
220           ''
221             if [ -n "${variable}" ]; then
222               ${ipcmd} add "${variable}" dev "$ifaceHost"
223             fi
224           ''
225         else
226           ''${ipcmd} add ${cfg.${attribute}} dev "$ifaceHost"'';
227       renderExtraVeth = name: cfg:
228         if cfg.hostBridge != null then
229           ''
230             # Add ${name} to bridge ${cfg.hostBridge}
231             ip link set dev "${name}" master "${cfg.hostBridge}" up
232           ''
233         else
234           ''
235             echo "Bring ${name} up"
236             ip link set dev "${name}" up
237             # Set IPs and routes for ${name}
238             ${optionalString (cfg.hostAddress != null) ''
239               ip addr add ${cfg.hostAddress} dev "${name}"
240             ''}
241             ${optionalString (cfg.hostAddress6 != null) ''
242               ip -6 addr add ${cfg.hostAddress6} dev "${name}"
243             ''}
244             ${optionalString (cfg.localAddress != null) ''
245               ip route add ${cfg.localAddress} dev "${name}"
246             ''}
247             ${optionalString (cfg.localAddress6 != null) ''
248               ip -6 route add ${cfg.localAddress6} dev "${name}"
249             ''}
250           '';
251     in
252       ''
253         if [ -n "$HOST_ADDRESS" ]  || [ -n "$LOCAL_ADDRESS" ] ||
254            [ -n "$HOST_ADDRESS6" ] || [ -n "$LOCAL_ADDRESS6" ]; then
255           if [ -z "$HOST_BRIDGE" ]; then
256             ifaceHost=ve-$INSTANCE
257             ip link set dev "$ifaceHost" up
259             ${ipcall cfg "ip addr" "$HOST_ADDRESS" "hostAddress"}
260             ${ipcall cfg "ip -6 addr" "$HOST_ADDRESS6" "hostAddress6"}
261             ${ipcall cfg "ip route" "$LOCAL_ADDRESS" "localAddress"}
262             ${ipcall cfg "ip -6 route" "$LOCAL_ADDRESS6" "localAddress6"}
263           fi
264         fi
265         ${concatStringsSep "\n" (mapAttrsToList renderExtraVeth cfg.extraVeths)}
266       ''
267   );
269   serviceDirectives = cfg: {
270     ExecReload = pkgs.writeScript "reload-container"
271       ''
272         #! ${pkgs.runtimeShell} -e
273         ${nixos-container}/bin/nixos-container run "$INSTANCE" -- \
274           bash --login -c "''${SYSTEM_PATH:-/nix/var/nix/profiles/system}/bin/switch-to-configuration test"
275       '';
277     SyslogIdentifier = "container %i";
279     EnvironmentFile = "-${configurationDirectory}/%i.conf";
281     Type = "notify";
283     RuntimeDirectory = lib.optional cfg.ephemeral "${configurationDirectoryName}/%i";
285     # Note that on reboot, systemd-nspawn returns 133, so this
286     # unit will be restarted. On poweroff, it returns 0, so the
287     # unit won't be restarted.
288     RestartForceExitStatus = "133";
289     SuccessExitStatus = "133";
291     # Some containers take long to start
292     # especially when you automatically start many at once
293     TimeoutStartSec = cfg.timeoutStartSec;
295     Restart = "on-failure";
297     Slice = "machine.slice";
298     Delegate = true;
300     # We rely on systemd-nspawn turning a SIGTERM to itself into a shutdown
301     # signal (SIGRTMIN+3) for the inner container.
302     KillMode = "mixed";
303     KillSignal = "TERM";
305     DevicePolicy = "closed";
306     DeviceAllow = map (d: "${d.node} ${d.modifier}") cfg.allowedDevices;
307   };
309   kernelVersion = config.boot.kernelPackages.kernel.version;
311   bindMountOpts = { name, ... }: {
313     options = {
314       mountPoint = mkOption {
315         example = "/mnt/usb";
316         type = types.str;
317         description = "Mount point on the container file system.";
318       };
319       hostPath = mkOption {
320         default = null;
321         example = "/home/alice";
322         type = types.nullOr types.str;
323         description = "Location of the host path to be mounted.";
324       };
325       isReadOnly = mkOption {
326         default = true;
327         type = types.bool;
328         description = "Determine whether the mounted path will be accessed in read-only mode.";
329       };
330     };
332     config = {
333       mountPoint = mkDefault name;
334     };
336   };
338   allowedDeviceOpts = { ... }: {
339     options = {
340       node = mkOption {
341         example = "/dev/net/tun";
342         type = types.str;
343         description = "Path to device node";
344       };
345       modifier = mkOption {
346         example = "rw";
347         type = types.str;
348         description = ''
349           Device node access modifier. Takes a combination
350           `r` (read), `w` (write), and
351           `m` (mknod). See the
352           `systemd.resource-control(5)` man page for more
353           information.'';
354       };
355     };
356   };
358   mkBindFlag = d:
359                let flagPrefix = if d.isReadOnly then " --bind-ro=" else " --bind=";
360                    mountstr = if d.hostPath != null then "${d.hostPath}:${d.mountPoint}" else "${d.mountPoint}";
361                in flagPrefix + mountstr ;
363   mkBindFlags = bs: concatMapStrings mkBindFlag (lib.attrValues bs);
365   networkOptions = {
366     hostBridge = mkOption {
367       type = types.nullOr types.str;
368       default = null;
369       example = "br0";
370       description = ''
371         Put the host-side of the veth-pair into the named bridge.
372         Only one of hostAddress* or hostBridge can be given.
373       '';
374     };
376     forwardPorts = mkOption {
377       type = types.listOf (types.submodule {
378         options = {
379           protocol = mkOption {
380             type = types.str;
381             default = "tcp";
382             description = "The protocol specifier for port forwarding between host and container";
383           };
384           hostPort = mkOption {
385             type = types.int;
386             description = "Source port of the external interface on host";
387           };
388           containerPort = mkOption {
389             type = types.nullOr types.int;
390             default = null;
391             description = "Target port of container";
392           };
393         };
394       });
395       default = [];
396       example = [ { protocol = "tcp"; hostPort = 8080; containerPort = 80; } ];
397       description = ''
398         List of forwarded ports from host to container. Each forwarded port
399         is specified by protocol, hostPort and containerPort. By default,
400         protocol is tcp and hostPort and containerPort are assumed to be
401         the same if containerPort is not explicitly given.
402       '';
403     };
406     hostAddress = mkOption {
407       type = types.nullOr types.str;
408       default = null;
409       example = "10.231.136.1";
410       description = ''
411         The IPv4 address assigned to the host interface.
412         (Not used when hostBridge is set.)
413       '';
414     };
416     hostAddress6 = mkOption {
417       type = types.nullOr types.str;
418       default = null;
419       example = "fc00::1";
420       description = ''
421         The IPv6 address assigned to the host interface.
422         (Not used when hostBridge is set.)
423       '';
424     };
426     localAddress = mkOption {
427       type = types.nullOr types.str;
428       default = null;
429       example = "10.231.136.2";
430       description = ''
431         The IPv4 address assigned to the interface in the container.
432         If a hostBridge is used, this should be given with netmask to access
433         the whole network. Otherwise the default netmask is /32 and routing is
434         set up from localAddress to hostAddress and back.
435       '';
436     };
438     localAddress6 = mkOption {
439       type = types.nullOr types.str;
440       default = null;
441       example = "fc00::2";
442       description = ''
443         The IPv6 address assigned to the interface in the container.
444         If a hostBridge is used, this should be given with netmask to access
445         the whole network. Otherwise the default netmask is /128 and routing is
446         set up from localAddress6 to hostAddress6 and back.
447       '';
448     };
450   };
452   dummyConfig =
453     {
454       extraVeths = {};
455       additionalCapabilities = [];
456       ephemeral = false;
457       timeoutStartSec = "1min";
458       allowedDevices = [];
459       hostAddress = null;
460       hostAddress6 = null;
461       localAddress = null;
462       localAddress6 = null;
463       tmpfs = null;
464     };
469   options = {
471     boot.isContainer = mkOption {
472       type = types.bool;
473       default = false;
474       description = ''
475         Whether this NixOS machine is a lightweight container running
476         in another NixOS system.
477       '';
478     };
480     boot.enableContainers = mkOption {
481       type = types.bool;
482       default = true;
483       description = ''
484         Whether to enable support for NixOS containers. Defaults to true
485         (at no cost if containers are not actually used).
486       '';
487     };
489     containers = mkOption {
490       type = types.attrsOf (types.submodule (
491         { config, options, name, ... }:
492         {
493           options = {
494             config = mkOption {
495               description = ''
496                 A specification of the desired configuration of this
497                 container, as a NixOS module.
498               '';
499               type = lib.mkOptionType {
500                 name = "Toplevel NixOS config";
501                 merge = loc: defs: (import "${toString config.nixpkgs}/nixos/lib/eval-config.nix" {
502                   modules =
503                     let
504                       extraConfig = { options, ... }: {
505                         _file = "module at ${__curPos.file}:${toString __curPos.line}";
506                         config = {
507                           nixpkgs =
508                             if options.nixpkgs?hostPlatform
509                             then { inherit (host.pkgs.stdenv) hostPlatform; }
510                             else { localSystem = host.pkgs.stdenv.hostPlatform; }
511                           ;
512                           boot.isContainer = true;
513                           networking.hostName = mkDefault name;
514                           networking.useDHCP = false;
515                           assertions = [
516                             {
517                               assertion =
518                                 (builtins.compareVersions kernelVersion "5.8" <= 0)
519                                 -> config.privateNetwork
520                                 -> stringLength name <= 11;
521                               message = ''
522                                 Container name `${name}` is too long: When `privateNetwork` is enabled, container names can
523                                 not be longer than 11 characters, because the container's interface name is derived from it.
524                                 You should either make the container name shorter or upgrade to a more recent kernel that
525                                 supports interface altnames (i.e. at least Linux 5.8 - please see https://github.com/NixOS/nixpkgs/issues/38509
526                                 for details).
527                               '';
528                             }
529                             {
530                               assertion = !lib.strings.hasInfix "_" name;
531                               message = ''
532                                 Names containing underscores are not allowed in nixos-containers. Please rename the container '${name}'
533                               '';
534                             }
535                           ];
536                         };
537                       };
538                     in [ extraConfig ] ++ (map (x: x.value) defs);
539                   prefix = [ "containers" name ];
540                   inherit (config) specialArgs;
542                   # The system is inherited from the host above.
543                   # Set it to null, to remove the "legacy" entrypoint's non-hermetic default.
544                   system = null;
545                 }).config;
546               };
547             };
549             path = mkOption {
550               type = types.path;
551               example = "/nix/var/nix/profiles/per-container/webserver";
552               description = ''
553                 As an alternative to specifying
554                 {option}`config`, you can specify the path to
555                 the evaluated NixOS system configuration, typically a
556                 symlink to a system profile.
557               '';
558             };
560             additionalCapabilities = mkOption {
561               type = types.listOf types.str;
562               default = [];
563               example = [ "CAP_NET_ADMIN" "CAP_MKNOD" ];
564               description = ''
565                 Grant additional capabilities to the container.  See the
566                 capabilities(7) and systemd-nspawn(1) man pages for more
567                 information.
568               '';
569             };
571             nixpkgs = mkOption {
572               type = types.path;
573               default = pkgs.path;
574               defaultText = literalExpression "pkgs.path";
575               description = ''
576                 A path to the nixpkgs that provide the modules, pkgs and lib for evaluating the container.
578                 To only change the `pkgs` argument used inside the container modules,
579                 set the `nixpkgs.*` options in the container {option}`config`.
580                 Setting `config.nixpkgs.pkgs = pkgs` speeds up the container evaluation
581                 by reusing the system pkgs, but the `nixpkgs.config` option in the
582                 container config is ignored in this case.
583               '';
584             };
586             specialArgs = mkOption {
587               type = types.attrsOf types.unspecified;
588               default = {};
589               description = ''
590                 A set of special arguments to be passed to NixOS modules.
591                 This will be merged into the `specialArgs` used to evaluate
592                 the NixOS configurations.
593               '';
594             };
596             ephemeral = mkOption {
597               type = types.bool;
598               default = false;
599               description = ''
600                 Runs container in ephemeral mode with the empty root filesystem at boot.
601                 This way container will be bootstrapped from scratch on each boot
602                 and will be cleaned up on shutdown leaving no traces behind.
603                 Useful for completely stateless, reproducible containers.
605                 Note that this option might require to do some adjustments to the container configuration,
606                 e.g. you might want to set
607                 {var}`systemd.network.networks.$interface.dhcpV4Config.ClientIdentifier` to "mac"
608                 if you use {var}`macvlans` option.
609                 This way dhcp client identifier will be stable between the container restarts.
611                 Note that the container journal will not be linked to the host if this option is enabled.
612               '';
613             };
615             enableTun = mkOption {
616               type = types.bool;
617               default = false;
618               description = ''
619                 Allows the container to create and setup tunnel interfaces
620                 by granting the `NET_ADMIN` capability and
621                 enabling access to `/dev/net/tun`.
622               '';
623             };
625             privateNetwork = mkOption {
626               type = types.bool;
627               default = false;
628               description = ''
629                 Whether to give the container its own private virtual
630                 Ethernet interface.  The interface is called
631                 `eth0`, and is hooked up to the interface
632                 `ve-«container-name»`
633                 on the host.  If this option is not set, then the
634                 container shares the network interfaces of the host,
635                 and can bind to any port on any interface.
636               '';
637             };
639             networkNamespace = mkOption {
640               type = types.nullOr types.path;
641               default = null;
642               description = ''
643                 Takes the path to a file representing a kernel network namespace that the container
644                 shall run in. The specified path should refer to a (possibly bind-mounted) network
645                 namespace file, as exposed by the kernel below /proc/<PID>/ns/net. This makes the
646                 container enter the given network namespace. One of the typical use cases is to give
647                 a network namespace under /run/netns created by ip-netns(8).
648                 Note that this option cannot be used together with other network-related options,
649                 such as --private-network or --network-interface=.
650               '';
651             };
653             interfaces = mkOption {
654               type = types.listOf types.str;
655               default = [];
656               example = [ "eth1" "eth2" ];
657               description = ''
658                 The list of interfaces to be moved into the container.
659               '';
660             };
662             macvlans = mkOption {
663               type = types.listOf types.str;
664               default = [];
665               example = [ "eth1" "eth2" ];
666               description = ''
667                 The list of host interfaces from which macvlans will be
668                 created. For each interface specified, a macvlan interface
669                 will be created and moved to the container.
670               '';
671             };
673             extraVeths = mkOption {
674               type = with types; attrsOf (submodule { options = networkOptions; });
675               default = {};
676               description = ''
677                 Extra veth-pairs to be created for the container.
678               '';
679             };
681             autoStart = mkOption {
682               type = types.bool;
683               default = false;
684               description = ''
685                 Whether the container is automatically started at boot-time.
686               '';
687             };
689             restartIfChanged = mkOption {
690               type = types.bool;
691               default = true;
692               description = ''
693                 Whether the container should be restarted during a NixOS
694                 configuration switch if its definition has changed.
695               '';
696             };
698             timeoutStartSec = mkOption {
699               type = types.str;
700               default = "1min";
701               description = ''
702                 Time for the container to start. In case of a timeout,
703                 the container processes get killed.
704                 See {manpage}`systemd.time(7)`
705                 for more information about the format.
706                '';
707             };
709             bindMounts = mkOption {
710               type = with types; attrsOf (submodule bindMountOpts);
711               default = {};
712               example = literalExpression ''
713                 { "/home" = { hostPath = "/home/alice";
714                               isReadOnly = false; };
715                 }
716               '';
718               description = ''
719                   An extra list of directories that is bound to the container.
720                 '';
721             };
723             allowedDevices = mkOption {
724               type = with types; listOf (submodule allowedDeviceOpts);
725               default = [];
726               example = [ { node = "/dev/net/tun"; modifier = "rwm"; } ];
727               description = ''
728                 A list of device nodes to which the containers has access to.
729               '';
730             };
732             tmpfs = mkOption {
733               type = types.listOf types.str;
734               default = [];
735               example = [ "/var" ];
736               description = ''
737                 Mounts a set of tmpfs file systems into the container.
738                 Multiple paths can be specified.
739                 Valid items must conform to the --tmpfs argument
740                 of systemd-nspawn. See systemd-nspawn(1) for details.
741               '';
742             };
744             extraFlags = mkOption {
745               type = types.listOf types.str;
746               default = [];
747               example = [ "--drop-capability=CAP_SYS_CHROOT" ];
748               description = ''
749                 Extra flags passed to the systemd-nspawn command.
750                 See systemd-nspawn(1) for details.
751               '';
752             };
754             # Removed option. See `checkAssertion` below for the accompanying error message.
755             pkgs = mkOption { visible = false; };
756           } // networkOptions;
758           config = let
759             # Throw an error when removed option `pkgs` is used.
760             # Because this is a submodule we cannot use `mkRemovedOptionModule` or option `assertions`.
761             optionPath = "containers.${name}.pkgs";
762             files = showFiles options.pkgs.files;
763             checkAssertion = if options.pkgs.isDefined then throw ''
764               The option definition `${optionPath}' in ${files} no longer has any effect; please remove it.
766               Alternatively, you can use the following options:
767               - containers.${name}.nixpkgs
768                 This sets the nixpkgs (and thereby the modules, pkgs and lib) that
769                 are used for evaluating the container.
771               - containers.${name}.config.nixpkgs.pkgs
772                 This only sets the `pkgs` argument used inside the container modules.
773             ''
774             else null;
775           in {
776             path = builtins.seq checkAssertion
777               mkIf options.config.isDefined config.config.system.build.toplevel;
778           };
779         }));
781       default = {};
782       example = literalExpression
783         ''
784           { webserver =
785               { path = "/nix/var/nix/profiles/webserver";
786               };
787             database =
788               { config =
789                   { config, pkgs, ... }:
790                   { services.postgresql.enable = true;
791                     services.postgresql.package = pkgs.postgresql_14;
793                     system.stateVersion = "${lib.trivial.release}";
794                   };
795               };
796           }
797         '';
798       description = ''
799         A set of NixOS system configurations to be run as lightweight
800         containers.  Each container appears as a service
801         `container-«name»`
802         on the host system, allowing it to be started and stopped via
803         {command}`systemctl`.
804       '';
805     };
807   };
810   config = mkMerge [
811     {
812       warnings = optional (!config.boot.enableContainers && config.containers != {})
813         "containers.<name> is used, but boot.enableContainers is false. To use containers.<name>, set boot.enableContainers to true.";
815       assertions = let
816         mapper = name: cfg: optional (cfg.networkNamespace != null && (cfg.privateNetwork || cfg.interfaces != []))
817           "containers.${name}.networkNamespace is mutally exclusive to containers.${name}.privateNetwork and containers.${name}.interfaces.";
818       in mkMerge (mapAttrsToList mapper config.containers);
819     }
821     (mkIf (config.boot.enableContainers) (let
822       unit = {
823         description = "Container '%i'";
825         unitConfig.RequiresMountsFor = "${stateDirectory}/%i";
827         path = [ pkgs.iproute2 ];
829         environment = {
830           root = "${stateDirectory}/%i";
831           INSTANCE = "%i";
832         };
834         preStart = preStartScript dummyConfig;
836         script = startScript dummyConfig;
838         postStart = postStartScript dummyConfig;
840         restartIfChanged = false;
842         serviceConfig = serviceDirectives dummyConfig;
843       };
844     in {
845       warnings =
846         (optional (config.virtualisation.containers.enable && versionOlder config.system.stateVersion "22.05") ''
847           Enabling both boot.enableContainers & virtualisation.containers on system.stateVersion < 22.05 is unsupported.
848         '');
850       systemd.targets.multi-user.wants = [ "machines.target" ];
852       systemd.services = listToAttrs (filter (x: x.value != null) (
853         # The generic container template used by imperative containers
854         [{ name = "container@"; value = unit; }]
855         # declarative containers
856         ++ (mapAttrsToList (name: cfg: nameValuePair "container@${name}" (let
857             containerConfig = cfg // (
858             optionalAttrs cfg.enableTun
859               {
860                 allowedDevices = cfg.allowedDevices
861                   ++ [ { node = "/dev/net/tun"; modifier = "rwm"; } ];
862                 additionalCapabilities = cfg.additionalCapabilities
863                   ++ [ "CAP_NET_ADMIN" ];
864               }
865             );
866           in
867             recursiveUpdate unit {
868               preStart = preStartScript containerConfig;
869               script = startScript containerConfig;
870               postStart = postStartScript containerConfig;
871               serviceConfig = serviceDirectives containerConfig;
872               unitConfig.RequiresMountsFor = lib.optional (!containerConfig.ephemeral) "${stateDirectory}/%i"
873                 ++ builtins.map
874                   (d: if d.hostPath != null then d.hostPath else d.mountPoint)
875                   (builtins.attrValues cfg.bindMounts);
876               environment.root = if containerConfig.ephemeral then "/run/nixos-containers/%i" else "${stateDirectory}/%i";
877             } // (
878             optionalAttrs containerConfig.autoStart
879               {
880                 wantedBy = [ "machines.target" ];
881                 wants = [ "network.target" ] ++ (map (i: "sys-subsystem-net-devices-${i}.device") cfg.interfaces);
882                 after = [ "network.target" ] ++ (map (i: "sys-subsystem-net-devices-${i}.device") cfg.interfaces);
883                 restartTriggers = [
884                   containerConfig.path
885                   config.environment.etc."${configurationDirectoryName}/${name}.conf".source
886                 ];
887                 restartIfChanged = containerConfig.restartIfChanged;
888               }
889             )
890         )) config.containers)
891       ));
893       # Generate a configuration file in /etc/nixos-containers for each
894       # container so that container@.target can get the container
895       # configuration.
896       environment.etc =
897         let mkPortStr = p: p.protocol + ":" + (toString p.hostPort) + ":" + (if p.containerPort == null then toString p.hostPort else toString p.containerPort);
898         in mapAttrs' (name: cfg: nameValuePair "${configurationDirectoryName}/${name}.conf"
899         { text =
900             ''
901               SYSTEM_PATH=${cfg.path}
902               ${optionalString cfg.privateNetwork ''
903                 PRIVATE_NETWORK=1
904                 ${optionalString (cfg.hostBridge != null) ''
905                   HOST_BRIDGE=${cfg.hostBridge}
906                 ''}
907                 ${optionalString (length cfg.forwardPorts > 0) ''
908                   HOST_PORT=${concatStringsSep "," (map mkPortStr cfg.forwardPorts)}
909                 ''}
910                 ${optionalString (cfg.hostAddress != null) ''
911                   HOST_ADDRESS=${cfg.hostAddress}
912                 ''}
913                 ${optionalString (cfg.hostAddress6 != null) ''
914                   HOST_ADDRESS6=${cfg.hostAddress6}
915                 ''}
916                 ${optionalString (cfg.localAddress != null) ''
917                   LOCAL_ADDRESS=${cfg.localAddress}
918                 ''}
919                 ${optionalString (cfg.localAddress6 != null) ''
920                   LOCAL_ADDRESS6=${cfg.localAddress6}
921                 ''}
922               ''}
923               ${optionalString (cfg.networkNamespace != null) ''
924                 NETWORK_NAMESPACE_PATH=${cfg.networkNamespace}
925               ''}
926               INTERFACES="${toString cfg.interfaces}"
927               MACVLANS="${toString cfg.macvlans}"
928               ${optionalString cfg.autoStart ''
929                 AUTO_START=1
930               ''}
931               EXTRA_NSPAWN_FLAGS="${mkBindFlags cfg.bindMounts +
932                 optionalString (cfg.extraFlags != [])
933                   (" " + concatStringsSep " " cfg.extraFlags)}"
934             '';
935         }) config.containers;
937       # Generate /etc/hosts entries for the containers.
938       networking.extraHosts = concatStrings (mapAttrsToList (name: cfg: optionalString (cfg.localAddress != null)
939         ''
940           ${head (splitString "/" cfg.localAddress)} ${name}.containers
941         '') config.containers);
943       networking.dhcpcd.denyInterfaces = [ "ve-*" "vb-*" ];
945       services.udev.extraRules = optionalString config.networking.networkmanager.enable ''
946         # Don't manage interfaces created by nixos-container.
947         ENV{INTERFACE}=="v[eb]-*", ENV{NM_UNMANAGED}="1"
948       '';
950       environment.systemPackages = [
951         nixos-container
952       ];
954       boot.kernelModules = [
955         "bridge"
956         "macvlan"
957         "tap"
958         "tun"
959       ];
960     }))
961   ];
963   meta.buildDocsInSandbox = false;