vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / virtualisation / nixos-containers.nix
blob812d22d773e1cd26a1d1565eecc0d1174daff4a2
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       extraFlags+=(${lib.escapeShellArgs (mapAttrsToList nspawnExtraVethArgs cfg.extraVeths)})
139       for iface in $INTERFACES; do
140         extraFlags+=("--network-interface=$iface")
141       done
143       for iface in $MACVLANS; do
144         extraFlags+=("--network-macvlan=$iface")
145       done
147       # If the host is 64-bit and the container is 32-bit, add a
148       # --personality flag.
149       ${optionalString (pkgs.stdenv.hostPlatform.system == "x86_64-linux") ''
150         if [ "$(< "''${SYSTEM_PATH:-/nix/var/nix/profiles/per-container/$INSTANCE/system}/system")" = i686-linux ]; then
151           extraFlags+=("--personality=x86")
152         fi
153       ''}
155       export SYSTEMD_NSPAWN_UNIFIED_HIERARCHY=1
157       # Run systemd-nspawn without startup notification (we'll
158       # wait for the container systemd to signal readiness)
159       # Kill signal handling means systemd-nspawn will pass a system-halt signal
160       # to the container systemd when it receives SIGTERM for container shutdown;
161       # containerInit and stage2 have to handle this as well.
162       # TODO: fix shellcheck issue properly
163       # shellcheck disable=SC2086
164       exec ${config.systemd.package}/bin/systemd-nspawn \
165         --keep-unit \
166         -M "$INSTANCE" -D "$root" "''${extraFlags[@]}" \
167         $EXTRA_NSPAWN_FLAGS \
168         --notify-ready=yes \
169         --kill-signal=SIGRTMIN+3 \
170         --bind-ro=/nix/store \
171         --bind-ro=/nix/var/nix/db \
172         --bind-ro=/nix/var/nix/daemon-socket \
173         --bind="/nix/var/nix/profiles/per-container/$INSTANCE:/nix/var/nix/profiles" \
174         --bind="/nix/var/nix/gcroots/per-container/$INSTANCE:/nix/var/nix/gcroots" \
175         ${optionalString (!cfg.ephemeral) "--link-journal=try-guest"} \
176         --setenv PRIVATE_NETWORK="$PRIVATE_NETWORK" \
177         --setenv HOST_BRIDGE="$HOST_BRIDGE" \
178         --setenv HOST_ADDRESS="$HOST_ADDRESS" \
179         --setenv LOCAL_ADDRESS="$LOCAL_ADDRESS" \
180         --setenv HOST_ADDRESS6="$HOST_ADDRESS6" \
181         --setenv LOCAL_ADDRESS6="$LOCAL_ADDRESS6" \
182         --setenv HOST_PORT="$HOST_PORT" \
183         --setenv PATH="$PATH" \
184         ${optionalString cfg.ephemeral "--ephemeral"} \
185         ${optionalString (cfg.additionalCapabilities != null && cfg.additionalCapabilities != [])
186           ''--capability="${concatStringsSep "," cfg.additionalCapabilities}"''
187         } \
188         ${optionalString (cfg.tmpfs != null && cfg.tmpfs != [])
189           ''--tmpfs=${concatStringsSep " --tmpfs=" cfg.tmpfs}''
190         } \
191         ${containerInit cfg} "''${SYSTEM_PATH:-/nix/var/nix/profiles/system}/init"
192     '';
194   preStartScript = cfg:
195     ''
196       # Clean up existing machined registration and interfaces.
197       machinectl terminate "$INSTANCE" 2> /dev/null || true
199       if [ -n "$HOST_ADDRESS" ]  || [ -n "$LOCAL_ADDRESS" ] ||
200          [ -n "$HOST_ADDRESS6" ] || [ -n "$LOCAL_ADDRESS6" ]; then
201         ip link del dev "ve-$INSTANCE" 2> /dev/null || true
202         ip link del dev "vb-$INSTANCE" 2> /dev/null || true
203       fi
205       ${concatStringsSep "\n" (
206         mapAttrsToList (name: cfg:
207           "ip link del dev ${name} 2> /dev/null || true "
208         ) cfg.extraVeths
209       )}
210    '';
212   postStartScript = (cfg:
213     let
214       ipcall = cfg: ipcmd: variable: attribute:
215         if cfg.${attribute} == null then
216           ''
217             if [ -n "${variable}" ]; then
218               ${ipcmd} add "${variable}" dev "$ifaceHost"
219             fi
220           ''
221         else
222           ''${ipcmd} add ${cfg.${attribute}} dev "$ifaceHost"'';
223       renderExtraVeth = name: cfg:
224         if cfg.hostBridge != null then
225           ''
226             # Add ${name} to bridge ${cfg.hostBridge}
227             ip link set dev "${name}" master "${cfg.hostBridge}" up
228           ''
229         else
230           ''
231             echo "Bring ${name} up"
232             ip link set dev "${name}" up
233             # Set IPs and routes for ${name}
234             ${optionalString (cfg.hostAddress != null) ''
235               ip addr add ${cfg.hostAddress} dev "${name}"
236             ''}
237             ${optionalString (cfg.hostAddress6 != null) ''
238               ip -6 addr add ${cfg.hostAddress6} dev "${name}"
239             ''}
240             ${optionalString (cfg.localAddress != null) ''
241               ip route add ${cfg.localAddress} dev "${name}"
242             ''}
243             ${optionalString (cfg.localAddress6 != null) ''
244               ip -6 route add ${cfg.localAddress6} dev "${name}"
245             ''}
246           '';
247     in
248       ''
249         if [ -n "$HOST_ADDRESS" ]  || [ -n "$LOCAL_ADDRESS" ] ||
250            [ -n "$HOST_ADDRESS6" ] || [ -n "$LOCAL_ADDRESS6" ]; then
251           if [ -z "$HOST_BRIDGE" ]; then
252             ifaceHost=ve-$INSTANCE
253             ip link set dev "$ifaceHost" up
255             ${ipcall cfg "ip addr" "$HOST_ADDRESS" "hostAddress"}
256             ${ipcall cfg "ip -6 addr" "$HOST_ADDRESS6" "hostAddress6"}
257             ${ipcall cfg "ip route" "$LOCAL_ADDRESS" "localAddress"}
258             ${ipcall cfg "ip -6 route" "$LOCAL_ADDRESS6" "localAddress6"}
259           fi
260         fi
261         ${concatStringsSep "\n" (mapAttrsToList renderExtraVeth cfg.extraVeths)}
262       ''
263   );
265   serviceDirectives = cfg: {
266     ExecReload = pkgs.writeScript "reload-container"
267       ''
268         #! ${pkgs.runtimeShell} -e
269         ${nixos-container}/bin/nixos-container run "$INSTANCE" -- \
270           bash --login -c "''${SYSTEM_PATH:-/nix/var/nix/profiles/system}/bin/switch-to-configuration test"
271       '';
273     SyslogIdentifier = "container %i";
275     EnvironmentFile = "-${configurationDirectory}/%i.conf";
277     Type = "notify";
279     RuntimeDirectory = lib.optional cfg.ephemeral "${configurationDirectoryName}/%i";
281     # Note that on reboot, systemd-nspawn returns 133, so this
282     # unit will be restarted. On poweroff, it returns 0, so the
283     # unit won't be restarted.
284     RestartForceExitStatus = "133";
285     SuccessExitStatus = "133";
287     # Some containers take long to start
288     # especially when you automatically start many at once
289     TimeoutStartSec = cfg.timeoutStartSec;
291     Restart = "on-failure";
293     Slice = "machine.slice";
294     Delegate = true;
296     # We rely on systemd-nspawn turning a SIGTERM to itself into a shutdown
297     # signal (SIGRTMIN+3) for the inner container.
298     KillMode = "mixed";
299     KillSignal = "TERM";
301     DevicePolicy = "closed";
302     DeviceAllow = map (d: "${d.node} ${d.modifier}") cfg.allowedDevices;
303   };
305   kernelVersion = config.boot.kernelPackages.kernel.version;
307   bindMountOpts = { name, ... }: {
309     options = {
310       mountPoint = mkOption {
311         example = "/mnt/usb";
312         type = types.str;
313         description = "Mount point on the container file system.";
314       };
315       hostPath = mkOption {
316         default = null;
317         example = "/home/alice";
318         type = types.nullOr types.str;
319         description = "Location of the host path to be mounted.";
320       };
321       isReadOnly = mkOption {
322         default = true;
323         type = types.bool;
324         description = "Determine whether the mounted path will be accessed in read-only mode.";
325       };
326     };
328     config = {
329       mountPoint = mkDefault name;
330     };
332   };
334   allowedDeviceOpts = { ... }: {
335     options = {
336       node = mkOption {
337         example = "/dev/net/tun";
338         type = types.str;
339         description = "Path to device node";
340       };
341       modifier = mkOption {
342         example = "rw";
343         type = types.str;
344         description = ''
345           Device node access modifier. Takes a combination
346           `r` (read), `w` (write), and
347           `m` (mknod). See the
348           `systemd.resource-control(5)` man page for more
349           information.'';
350       };
351     };
352   };
354   mkBindFlag = d:
355                let flagPrefix = if d.isReadOnly then " --bind-ro=" else " --bind=";
356                    mountstr = if d.hostPath != null then "${d.hostPath}:${d.mountPoint}" else "${d.mountPoint}";
357                in flagPrefix + mountstr ;
359   mkBindFlags = bs: concatMapStrings mkBindFlag (lib.attrValues bs);
361   networkOptions = {
362     hostBridge = mkOption {
363       type = types.nullOr types.str;
364       default = null;
365       example = "br0";
366       description = ''
367         Put the host-side of the veth-pair into the named bridge.
368         Only one of hostAddress* or hostBridge can be given.
369       '';
370     };
372     forwardPorts = mkOption {
373       type = types.listOf (types.submodule {
374         options = {
375           protocol = mkOption {
376             type = types.str;
377             default = "tcp";
378             description = "The protocol specifier for port forwarding between host and container";
379           };
380           hostPort = mkOption {
381             type = types.int;
382             description = "Source port of the external interface on host";
383           };
384           containerPort = mkOption {
385             type = types.nullOr types.int;
386             default = null;
387             description = "Target port of container";
388           };
389         };
390       });
391       default = [];
392       example = [ { protocol = "tcp"; hostPort = 8080; containerPort = 80; } ];
393       description = ''
394         List of forwarded ports from host to container. Each forwarded port
395         is specified by protocol, hostPort and containerPort. By default,
396         protocol is tcp and hostPort and containerPort are assumed to be
397         the same if containerPort is not explicitly given.
398       '';
399     };
402     hostAddress = mkOption {
403       type = types.nullOr types.str;
404       default = null;
405       example = "10.231.136.1";
406       description = ''
407         The IPv4 address assigned to the host interface.
408         (Not used when hostBridge is set.)
409       '';
410     };
412     hostAddress6 = mkOption {
413       type = types.nullOr types.str;
414       default = null;
415       example = "fc00::1";
416       description = ''
417         The IPv6 address assigned to the host interface.
418         (Not used when hostBridge is set.)
419       '';
420     };
422     localAddress = mkOption {
423       type = types.nullOr types.str;
424       default = null;
425       example = "10.231.136.2";
426       description = ''
427         The IPv4 address assigned to the interface in the container.
428         If a hostBridge is used, this should be given with netmask to access
429         the whole network. Otherwise the default netmask is /32 and routing is
430         set up from localAddress to hostAddress and back.
431       '';
432     };
434     localAddress6 = mkOption {
435       type = types.nullOr types.str;
436       default = null;
437       example = "fc00::2";
438       description = ''
439         The IPv6 address assigned to the interface in the container.
440         If a hostBridge is used, this should be given with netmask to access
441         the whole network. Otherwise the default netmask is /128 and routing is
442         set up from localAddress6 to hostAddress6 and back.
443       '';
444     };
446   };
448   dummyConfig =
449     {
450       extraVeths = {};
451       additionalCapabilities = [];
452       ephemeral = false;
453       timeoutStartSec = "1min";
454       allowedDevices = [];
455       hostAddress = null;
456       hostAddress6 = null;
457       localAddress = null;
458       localAddress6 = null;
459       tmpfs = null;
460     };
465   options = {
467     boot.isContainer = mkOption {
468       type = types.bool;
469       default = false;
470       description = ''
471         Whether this NixOS machine is a lightweight container running
472         in another NixOS system.
473       '';
474     };
476     boot.enableContainers = mkOption {
477       type = types.bool;
478       default = true;
479       description = ''
480         Whether to enable support for NixOS containers. Defaults to true
481         (at no cost if containers are not actually used).
482       '';
483     };
485     containers = mkOption {
486       type = types.attrsOf (types.submodule (
487         { config, options, name, ... }:
488         {
489           options = {
490             config = mkOption {
491               description = ''
492                 A specification of the desired configuration of this
493                 container, as a NixOS module.
494               '';
495               type = lib.mkOptionType {
496                 name = "Toplevel NixOS config";
497                 merge = loc: defs: (import "${toString config.nixpkgs}/nixos/lib/eval-config.nix" {
498                   modules =
499                     let
500                       extraConfig = { options, ... }: {
501                         _file = "module at ${__curPos.file}:${toString __curPos.line}";
502                         config = {
503                           nixpkgs =
504                             if options.nixpkgs?hostPlatform
505                             then { inherit (host.pkgs.stdenv) hostPlatform; }
506                             else { localSystem = host.pkgs.stdenv.hostPlatform; }
507                           ;
508                           boot.isContainer = true;
509                           networking.hostName = mkDefault name;
510                           networking.useDHCP = false;
511                           assertions = [
512                             {
513                               assertion =
514                                 (builtins.compareVersions kernelVersion "5.8" <= 0)
515                                 -> config.privateNetwork
516                                 -> stringLength name <= 11;
517                               message = ''
518                                 Container name `${name}` is too long: When `privateNetwork` is enabled, container names can
519                                 not be longer than 11 characters, because the container's interface name is derived from it.
520                                 You should either make the container name shorter or upgrade to a more recent kernel that
521                                 supports interface altnames (i.e. at least Linux 5.8 - please see https://github.com/NixOS/nixpkgs/issues/38509
522                                 for details).
523                               '';
524                             }
525                             {
526                               assertion = !lib.strings.hasInfix "_" name;
527                               message = ''
528                                 Names containing underscores are not allowed in nixos-containers. Please rename the container '${name}'
529                               '';
530                             }
531                           ];
532                         };
533                       };
534                     in [ extraConfig ] ++ (map (x: x.value) defs);
535                   prefix = [ "containers" name ];
536                   inherit (config) specialArgs;
538                   # The system is inherited from the host above.
539                   # Set it to null, to remove the "legacy" entrypoint's non-hermetic default.
540                   system = null;
541                 }).config;
542               };
543             };
545             path = mkOption {
546               type = types.path;
547               example = "/nix/var/nix/profiles/per-container/webserver";
548               description = ''
549                 As an alternative to specifying
550                 {option}`config`, you can specify the path to
551                 the evaluated NixOS system configuration, typically a
552                 symlink to a system profile.
553               '';
554             };
556             additionalCapabilities = mkOption {
557               type = types.listOf types.str;
558               default = [];
559               example = [ "CAP_NET_ADMIN" "CAP_MKNOD" ];
560               description = ''
561                 Grant additional capabilities to the container.  See the
562                 capabilities(7) and systemd-nspawn(1) man pages for more
563                 information.
564               '';
565             };
567             nixpkgs = mkOption {
568               type = types.path;
569               default = pkgs.path;
570               defaultText = literalExpression "pkgs.path";
571               description = ''
572                 A path to the nixpkgs that provide the modules, pkgs and lib for evaluating the container.
574                 To only change the `pkgs` argument used inside the container modules,
575                 set the `nixpkgs.*` options in the container {option}`config`.
576                 Setting `config.nixpkgs.pkgs = pkgs` speeds up the container evaluation
577                 by reusing the system pkgs, but the `nixpkgs.config` option in the
578                 container config is ignored in this case.
579               '';
580             };
582             specialArgs = mkOption {
583               type = types.attrsOf types.unspecified;
584               default = {};
585               description = ''
586                 A set of special arguments to be passed to NixOS modules.
587                 This will be merged into the `specialArgs` used to evaluate
588                 the NixOS configurations.
589               '';
590             };
592             ephemeral = mkOption {
593               type = types.bool;
594               default = false;
595               description = ''
596                 Runs container in ephemeral mode with the empty root filesystem at boot.
597                 This way container will be bootstrapped from scratch on each boot
598                 and will be cleaned up on shutdown leaving no traces behind.
599                 Useful for completely stateless, reproducible containers.
601                 Note that this option might require to do some adjustments to the container configuration,
602                 e.g. you might want to set
603                 {var}`systemd.network.networks.$interface.dhcpV4Config.ClientIdentifier` to "mac"
604                 if you use {var}`macvlans` option.
605                 This way dhcp client identifier will be stable between the container restarts.
607                 Note that the container journal will not be linked to the host if this option is enabled.
608               '';
609             };
611             enableTun = mkOption {
612               type = types.bool;
613               default = false;
614               description = ''
615                 Allows the container to create and setup tunnel interfaces
616                 by granting the `NET_ADMIN` capability and
617                 enabling access to `/dev/net/tun`.
618               '';
619             };
621             privateNetwork = mkOption {
622               type = types.bool;
623               default = false;
624               description = ''
625                 Whether to give the container its own private virtual
626                 Ethernet interface.  The interface is called
627                 `eth0`, and is hooked up to the interface
628                 `ve-«container-name»`
629                 on the host.  If this option is not set, then the
630                 container shares the network interfaces of the host,
631                 and can bind to any port on any interface.
632               '';
633             };
635             interfaces = mkOption {
636               type = types.listOf types.str;
637               default = [];
638               example = [ "eth1" "eth2" ];
639               description = ''
640                 The list of interfaces to be moved into the container.
641               '';
642             };
644             macvlans = mkOption {
645               type = types.listOf types.str;
646               default = [];
647               example = [ "eth1" "eth2" ];
648               description = ''
649                 The list of host interfaces from which macvlans will be
650                 created. For each interface specified, a macvlan interface
651                 will be created and moved to the container.
652               '';
653             };
655             extraVeths = mkOption {
656               type = with types; attrsOf (submodule { options = networkOptions; });
657               default = {};
658               description = ''
659                 Extra veth-pairs to be created for the container.
660               '';
661             };
663             autoStart = mkOption {
664               type = types.bool;
665               default = false;
666               description = ''
667                 Whether the container is automatically started at boot-time.
668               '';
669             };
671             restartIfChanged = mkOption {
672               type = types.bool;
673               default = true;
674               description = ''
675                 Whether the container should be restarted during a NixOS
676                 configuration switch if its definition has changed.
677               '';
678             };
680             timeoutStartSec = mkOption {
681               type = types.str;
682               default = "1min";
683               description = ''
684                 Time for the container to start. In case of a timeout,
685                 the container processes get killed.
686                 See {manpage}`systemd.time(7)`
687                 for more information about the format.
688                '';
689             };
691             bindMounts = mkOption {
692               type = with types; attrsOf (submodule bindMountOpts);
693               default = {};
694               example = literalExpression ''
695                 { "/home" = { hostPath = "/home/alice";
696                               isReadOnly = false; };
697                 }
698               '';
700               description = ''
701                   An extra list of directories that is bound to the container.
702                 '';
703             };
705             allowedDevices = mkOption {
706               type = with types; listOf (submodule allowedDeviceOpts);
707               default = [];
708               example = [ { node = "/dev/net/tun"; modifier = "rw"; } ];
709               description = ''
710                 A list of device nodes to which the containers has access to.
711               '';
712             };
714             tmpfs = mkOption {
715               type = types.listOf types.str;
716               default = [];
717               example = [ "/var" ];
718               description = ''
719                 Mounts a set of tmpfs file systems into the container.
720                 Multiple paths can be specified.
721                 Valid items must conform to the --tmpfs argument
722                 of systemd-nspawn. See systemd-nspawn(1) for details.
723               '';
724             };
726             extraFlags = mkOption {
727               type = types.listOf types.str;
728               default = [];
729               example = [ "--drop-capability=CAP_SYS_CHROOT" ];
730               description = ''
731                 Extra flags passed to the systemd-nspawn command.
732                 See systemd-nspawn(1) for details.
733               '';
734             };
736             # Removed option. See `checkAssertion` below for the accompanying error message.
737             pkgs = mkOption { visible = false; };
738           } // networkOptions;
740           config = let
741             # Throw an error when removed option `pkgs` is used.
742             # Because this is a submodule we cannot use `mkRemovedOptionModule` or option `assertions`.
743             optionPath = "containers.${name}.pkgs";
744             files = showFiles options.pkgs.files;
745             checkAssertion = if options.pkgs.isDefined then throw ''
746               The option definition `${optionPath}' in ${files} no longer has any effect; please remove it.
748               Alternatively, you can use the following options:
749               - containers.${name}.nixpkgs
750                 This sets the nixpkgs (and thereby the modules, pkgs and lib) that
751                 are used for evaluating the container.
753               - containers.${name}.config.nixpkgs.pkgs
754                 This only sets the `pkgs` argument used inside the container modules.
755             ''
756             else null;
757           in {
758             path = builtins.seq checkAssertion
759               mkIf options.config.isDefined config.config.system.build.toplevel;
760           };
761         }));
763       default = {};
764       example = literalExpression
765         ''
766           { webserver =
767               { path = "/nix/var/nix/profiles/webserver";
768               };
769             database =
770               { config =
771                   { config, pkgs, ... }:
772                   { services.postgresql.enable = true;
773                     services.postgresql.package = pkgs.postgresql_14;
775                     system.stateVersion = "${lib.trivial.release}";
776                   };
777               };
778           }
779         '';
780       description = ''
781         A set of NixOS system configurations to be run as lightweight
782         containers.  Each container appears as a service
783         `container-«name»`
784         on the host system, allowing it to be started and stopped via
785         {command}`systemctl`.
786       '';
787     };
789   };
792   config = mkMerge [
793     {
794       warnings = optional (!config.boot.enableContainers && config.containers != {})
795         "containers.<name> is used, but boot.enableContainers is false. To use containers.<name>, set boot.enableContainers to true.";
796     }
798     (mkIf (config.boot.enableContainers) (let
799       unit = {
800         description = "Container '%i'";
802         unitConfig.RequiresMountsFor = "${stateDirectory}/%i";
804         path = [ pkgs.iproute2 ];
806         environment = {
807           root = "${stateDirectory}/%i";
808           INSTANCE = "%i";
809         };
811         preStart = preStartScript dummyConfig;
813         script = startScript dummyConfig;
815         postStart = postStartScript dummyConfig;
817         restartIfChanged = false;
819         serviceConfig = serviceDirectives dummyConfig;
820       };
821     in {
822       warnings =
823         (optional (config.virtualisation.containers.enable && versionOlder config.system.stateVersion "22.05") ''
824           Enabling both boot.enableContainers & virtualisation.containers on system.stateVersion < 22.05 is unsupported.
825         '');
827       systemd.targets.multi-user.wants = [ "machines.target" ];
829       systemd.services = listToAttrs (filter (x: x.value != null) (
830         # The generic container template used by imperative containers
831         [{ name = "container@"; value = unit; }]
832         # declarative containers
833         ++ (mapAttrsToList (name: cfg: nameValuePair "container@${name}" (let
834             containerConfig = cfg // (
835             optionalAttrs cfg.enableTun
836               {
837                 allowedDevices = cfg.allowedDevices
838                   ++ [ { node = "/dev/net/tun"; modifier = "rw"; } ];
839                 additionalCapabilities = cfg.additionalCapabilities
840                   ++ [ "CAP_NET_ADMIN" ];
841               }
842             );
843           in
844             recursiveUpdate unit {
845               preStart = preStartScript containerConfig;
846               script = startScript containerConfig;
847               postStart = postStartScript containerConfig;
848               serviceConfig = serviceDirectives containerConfig;
849               unitConfig.RequiresMountsFor = lib.optional (!containerConfig.ephemeral) "${stateDirectory}/%i"
850                 ++ builtins.map
851                   (d: if d.hostPath != null then d.hostPath else d.mountPoint)
852                   (builtins.attrValues cfg.bindMounts);
853               environment.root = if containerConfig.ephemeral then "/run/nixos-containers/%i" else "${stateDirectory}/%i";
854             } // (
855             optionalAttrs containerConfig.autoStart
856               {
857                 wantedBy = [ "machines.target" ];
858                 wants = [ "network.target" ] ++ (map (i: "sys-subsystem-net-devices-${i}.device") cfg.interfaces);
859                 after = [ "network.target" ] ++ (map (i: "sys-subsystem-net-devices-${i}.device") cfg.interfaces);
860                 restartTriggers = [
861                   containerConfig.path
862                   config.environment.etc."${configurationDirectoryName}/${name}.conf".source
863                 ];
864                 restartIfChanged = containerConfig.restartIfChanged;
865               }
866             )
867         )) config.containers)
868       ));
870       # Generate a configuration file in /etc/nixos-containers for each
871       # container so that container@.target can get the container
872       # configuration.
873       environment.etc =
874         let mkPortStr = p: p.protocol + ":" + (toString p.hostPort) + ":" + (if p.containerPort == null then toString p.hostPort else toString p.containerPort);
875         in mapAttrs' (name: cfg: nameValuePair "${configurationDirectoryName}/${name}.conf"
876         { text =
877             ''
878               SYSTEM_PATH=${cfg.path}
879               ${optionalString cfg.privateNetwork ''
880                 PRIVATE_NETWORK=1
881                 ${optionalString (cfg.hostBridge != null) ''
882                   HOST_BRIDGE=${cfg.hostBridge}
883                 ''}
884                 ${optionalString (length cfg.forwardPorts > 0) ''
885                   HOST_PORT=${concatStringsSep "," (map mkPortStr cfg.forwardPorts)}
886                 ''}
887                 ${optionalString (cfg.hostAddress != null) ''
888                   HOST_ADDRESS=${cfg.hostAddress}
889                 ''}
890                 ${optionalString (cfg.hostAddress6 != null) ''
891                   HOST_ADDRESS6=${cfg.hostAddress6}
892                 ''}
893                 ${optionalString (cfg.localAddress != null) ''
894                   LOCAL_ADDRESS=${cfg.localAddress}
895                 ''}
896                 ${optionalString (cfg.localAddress6 != null) ''
897                   LOCAL_ADDRESS6=${cfg.localAddress6}
898                 ''}
899               ''}
900               INTERFACES="${toString cfg.interfaces}"
901               MACVLANS="${toString cfg.macvlans}"
902               ${optionalString cfg.autoStart ''
903                 AUTO_START=1
904               ''}
905               EXTRA_NSPAWN_FLAGS="${mkBindFlags cfg.bindMounts +
906                 optionalString (cfg.extraFlags != [])
907                   (" " + concatStringsSep " " cfg.extraFlags)}"
908             '';
909         }) config.containers;
911       # Generate /etc/hosts entries for the containers.
912       networking.extraHosts = concatStrings (mapAttrsToList (name: cfg: optionalString (cfg.localAddress != null)
913         ''
914           ${head (splitString "/" cfg.localAddress)} ${name}.containers
915         '') config.containers);
917       networking.dhcpcd.denyInterfaces = [ "ve-*" "vb-*" ];
919       services.udev.extraRules = optionalString config.networking.networkmanager.enable ''
920         # Don't manage interfaces created by nixos-container.
921         ENV{INTERFACE}=="v[eb]-*", ENV{NM_UNMANAGED}="1"
922       '';
924       environment.systemPackages = [
925         nixos-container
926       ];
928       boot.kernelModules = [
929         "bridge"
930         "macvlan"
931         "tap"
932         "tun"
933       ];
934     }))
935   ];
937   meta.buildDocsInSandbox = false;