at 25.11-pre 24 kB view raw
1{ 2 config, 3 options, 4 lib, 5 pkgs, 6 ... 7}: 8 9with lib; 10let 11 cfg = config.virtualisation.oci-containers; 12 proxy_env = config.networking.proxy.envVars; 13 14 defaultBackend = options.virtualisation.oci-containers.backend.default; 15 16 containerOptions = 17 { name, ... }: 18 { 19 20 config = { 21 podman = mkIf (cfg.backend == "podman") { }; 22 }; 23 24 options = { 25 26 image = mkOption { 27 type = with types; str; 28 description = "OCI image to run."; 29 example = "library/hello-world"; 30 }; 31 32 imageFile = mkOption { 33 type = with types; nullOr package; 34 default = null; 35 description = '' 36 Path to an image file to load before running the image. This can 37 be used to bypass pulling the image from the registry. 38 39 The `image` attribute must match the name and 40 tag of the image contained in this file, as they will be used to 41 run the container with that image. If they do not match, the 42 image will be pulled from the registry as usual. 43 ''; 44 example = literalExpression "pkgs.dockerTools.buildImage {...};"; 45 }; 46 47 imageStream = mkOption { 48 type = with types; nullOr package; 49 default = null; 50 description = '' 51 Path to a script that streams the desired image on standard output. 52 53 This option is mainly intended for use with 54 `pkgs.dockerTools.streamLayeredImage` so that the intermediate 55 image archive does not need to be stored in the Nix store. For 56 larger images this optimization can significantly reduce Nix store 57 churn compared to using the `imageFile` option, because you don't 58 have to store a new copy of the image archive in the Nix store 59 every time you change the image. Instead, if you stream the image 60 then you only need to build and store the layers that differ from 61 the previous image. 62 ''; 63 example = literalExpression "pkgs.dockerTools.streamLayeredImage {...};"; 64 }; 65 66 serviceName = mkOption { 67 type = types.str; 68 default = "${cfg.backend}-${name}"; 69 defaultText = "<backend>-<name>"; 70 description = "Systemd service name that manages the container"; 71 }; 72 73 login = { 74 75 username = mkOption { 76 type = with types; nullOr str; 77 default = null; 78 description = "Username for login."; 79 }; 80 81 passwordFile = mkOption { 82 type = with types; nullOr str; 83 default = null; 84 description = "Path to file containing password."; 85 example = "/etc/nixos/dockerhub-password.txt"; 86 }; 87 88 registry = mkOption { 89 type = with types; nullOr str; 90 default = null; 91 description = "Registry where to login to."; 92 example = "https://docker.pkg.github.com"; 93 }; 94 95 }; 96 97 cmd = mkOption { 98 type = with types; listOf str; 99 default = [ ]; 100 description = "Commandline arguments to pass to the image's entrypoint."; 101 example = literalExpression '' 102 ["--port=9000"] 103 ''; 104 }; 105 106 labels = mkOption { 107 type = with types; attrsOf str; 108 default = { }; 109 description = "Labels to attach to the container at runtime."; 110 example = literalExpression '' 111 { 112 "traefik.https.routers.example.rule" = "Host(`example.container`)"; 113 } 114 ''; 115 }; 116 117 entrypoint = mkOption { 118 type = with types; nullOr str; 119 description = "Override the default entrypoint of the image."; 120 default = null; 121 example = "/bin/my-app"; 122 }; 123 124 environment = mkOption { 125 type = with types; attrsOf str; 126 default = { }; 127 description = "Environment variables to set for this container."; 128 example = literalExpression '' 129 { 130 DATABASE_HOST = "db.example.com"; 131 DATABASE_PORT = "3306"; 132 } 133 ''; 134 }; 135 136 environmentFiles = mkOption { 137 type = with types; listOf path; 138 default = [ ]; 139 description = "Environment files for this container."; 140 example = literalExpression '' 141 [ 142 /path/to/.env 143 /path/to/.env.secret 144 ] 145 ''; 146 }; 147 148 log-driver = mkOption { 149 type = types.str; 150 default = "journald"; 151 description = '' 152 Logging driver for the container. The default of 153 `"journald"` means that the container's logs will be 154 handled as part of the systemd unit. 155 156 For more details and a full list of logging drivers, refer to respective backends documentation. 157 158 For Docker: 159 [Docker engine documentation](https://docs.docker.com/engine/logging/configure/) 160 161 For Podman: 162 Refer to the {manpage}`docker-run(1)` man page. 163 ''; 164 }; 165 166 ports = mkOption { 167 type = with types; listOf str; 168 default = [ ]; 169 description = '' 170 Network ports to publish from the container to the outer host. 171 172 Valid formats: 173 - `<ip>:<hostPort>:<containerPort>` 174 - `<ip>::<containerPort>` 175 - `<hostPort>:<containerPort>` 176 - `<containerPort>` 177 178 Both `hostPort` and `containerPort` can be specified as a range of 179 ports. When specifying ranges for both, the number of container 180 ports in the range must match the number of host ports in the 181 range. Example: `1234-1236:1234-1236/tcp` 182 183 When specifying a range for `hostPort` only, the `containerPort` 184 must *not* be a range. In this case, the container port is published 185 somewhere within the specified `hostPort` range. 186 Example: `1234-1236:1234/tcp` 187 188 Publishing a port bypasses the NixOS firewall. If the port is not 189 supposed to be shared on the network, make sure to publish the 190 port to localhost. 191 Example: `127.0.0.1:1234:1234` 192 193 Refer to the 194 [Docker engine documentation](https://docs.docker.com/engine/network/#published-ports) for full details. 195 ''; 196 example = literalExpression '' 197 [ 198 "127.0.0.1:8080:9000" 199 ] 200 ''; 201 }; 202 203 user = mkOption { 204 type = with types; nullOr str; 205 default = null; 206 description = '' 207 Override the username or UID (and optionally groupname or GID) used 208 in the container. 209 ''; 210 example = "nobody:nogroup"; 211 }; 212 213 volumes = mkOption { 214 type = with types; listOf str; 215 default = [ ]; 216 description = '' 217 List of volumes to attach to this container. 218 219 Note that this is a list of `"src:dst"` strings to 220 allow for `src` to refer to `/nix/store` paths, which 221 would be difficult with an attribute set. There are 222 also a variety of mount options available as a third 223 field; please refer to the 224 [docker engine documentation](https://docs.docker.com/engine/storage/volumes/) for details. 225 ''; 226 example = literalExpression '' 227 [ 228 "volume_name:/path/inside/container" 229 "/path/on/host:/path/inside/container" 230 ] 231 ''; 232 }; 233 234 workdir = mkOption { 235 type = with types; nullOr str; 236 default = null; 237 description = "Override the default working directory for the container."; 238 example = "/var/lib/hello_world"; 239 }; 240 241 dependsOn = mkOption { 242 type = with types; listOf str; 243 default = [ ]; 244 description = '' 245 Define which other containers this one depends on. They will be added to both After and Requires for the unit. 246 247 Use the same name as the attribute under `virtualisation.oci-containers.containers`. 248 ''; 249 example = literalExpression '' 250 virtualisation.oci-containers.containers = { 251 node1 = {}; 252 node2 = { 253 dependsOn = [ "node1" ]; 254 } 255 } 256 ''; 257 }; 258 259 hostname = mkOption { 260 type = with types; nullOr str; 261 default = null; 262 description = "The hostname of the container."; 263 example = "hello-world"; 264 }; 265 266 preRunExtraOptions = mkOption { 267 type = with types; listOf str; 268 default = [ ]; 269 description = "Extra options for {command}`${defaultBackend}` that go before the `run` argument."; 270 example = [ 271 "--runtime" 272 "runsc" 273 ]; 274 }; 275 276 extraOptions = mkOption { 277 type = with types; listOf str; 278 default = [ ]; 279 description = "Extra options for {command}`${defaultBackend} run`."; 280 example = literalExpression '' 281 ["--network=host"] 282 ''; 283 }; 284 285 autoStart = mkOption { 286 type = with types; bool; 287 default = true; 288 description = '' 289 When enabled, the container is automatically started on boot. 290 If this option is set to false, the container has to be started on-demand via its service. 291 ''; 292 }; 293 294 podman = mkOption { 295 type = types.nullOr ( 296 types.submodule { 297 options = { 298 sdnotify = mkOption { 299 default = "conmon"; 300 type = types.enum [ 301 "conmon" 302 "healthy" 303 "container" 304 ]; 305 description = '' 306 Determines how `podman` should notify systemd that the unit is ready. There are 307 [three options](https://docs.podman.io/en/latest/markdown/podman-run.1.html#sdnotify-container-conmon-healthy-ignore): 308 309 * `conmon`: marks the unit as ready when the container has started. 310 * `healthy`: marks the unit as ready when the [container's healthcheck](https://docs.podman.io/en/stable/markdown/podman-healthcheck-run.1.html) passes. 311 * `container`: `NOTIFY_SOCKET` is passed into the container and the process inside the container needs to indicate on its own that it's ready. 312 ''; 313 }; 314 user = mkOption { 315 default = "root"; 316 type = types.str; 317 description = '' 318 The user under which the container should run. 319 ''; 320 }; 321 }; 322 } 323 ); 324 default = null; 325 description = '' 326 Podman-specific settings in OCI containers. These must be null when using 327 the `docker` backend. 328 ''; 329 }; 330 331 pull = mkOption { 332 type = 333 with types; 334 enum [ 335 "always" 336 "missing" 337 "never" 338 "newer" 339 ]; 340 default = "missing"; 341 description = '' 342 Image pull policy for the container. Must be one of: always, missing, never, newer 343 ''; 344 }; 345 346 capabilities = mkOption { 347 type = with types; lazyAttrsOf (nullOr bool); 348 default = { }; 349 description = '' 350 Capabilities to configure for the container. 351 When set to true, capability is added to the container. 352 When set to false, capability is dropped from the container. 353 When null, default runtime settings apply. 354 ''; 355 example = literalExpression '' 356 { 357 SYS_ADMIN = true; 358 SYS_WRITE = false; 359 { 360 ''; 361 }; 362 363 devices = mkOption { 364 type = with types; listOf str; 365 default = [ ]; 366 description = '' 367 List of devices to attach to this container. 368 ''; 369 example = literalExpression '' 370 [ 371 "/dev/dri:/dev/dri" 372 ] 373 ''; 374 }; 375 376 privileged = mkOption { 377 type = with types; bool; 378 default = false; 379 description = '' 380 Give extended privileges to the container 381 ''; 382 }; 383 384 autoRemoveOnStop = mkOption { 385 type = types.bool; 386 default = true; 387 description = '' 388 Automatically remove the container when it is stopped or killed 389 ''; 390 }; 391 392 networks = mkOption { 393 type = with types; listOf str; 394 default = [ ]; 395 description = '' 396 Networks to attach the container to 397 ''; 398 }; 399 }; 400 }; 401 402 isValidLogin = 403 login: login.username != null && login.passwordFile != null && login.registry != null; 404 405 mkService = 406 name: container: 407 let 408 dependsOn = map (x: "${cfg.backend}-${x}.service") container.dependsOn; 409 escapedName = escapeShellArg name; 410 preStartScript = pkgs.writeShellApplication { 411 name = "pre-start"; 412 runtimeInputs = [ ]; 413 text = '' 414 ${cfg.backend} rm -f ${name} || true 415 ${optionalString (isValidLogin container.login) '' 416 # try logging in, if it fails, check if image exists locally 417 ${cfg.backend} login \ 418 ${container.login.registry} \ 419 --username ${escapeShellArg container.login.username} \ 420 --password-stdin < ${container.login.passwordFile} \ 421 || ${cfg.backend} image inspect ${container.image} >/dev/null \ 422 || { echo "image doesn't exist locally and login failed" >&2 ; exit 1; } 423 ''} 424 ${optionalString (container.imageFile != null) '' 425 ${cfg.backend} load -i ${container.imageFile} 426 ''} 427 ${optionalString (container.imageStream != null) '' 428 ${container.imageStream} | ${cfg.backend} load 429 ''} 430 ${optionalString (cfg.backend == "podman") '' 431 rm -f /run/${escapedName}/ctr-id 432 ''} 433 ''; 434 }; 435 436 effectiveUser = container.podman.user or "root"; 437 inherit (config.users.users.${effectiveUser}) uid; 438 dependOnLingerService = 439 cfg.backend == "podman" && effectiveUser != "root" && config.users.users.${effectiveUser}.linger; 440 in 441 { 442 wantedBy = [ ] ++ optional (container.autoStart) "multi-user.target"; 443 wants = 444 lib.optional (container.imageFile == null && container.imageStream == null) "network-online.target" 445 ++ lib.optionals dependOnLingerService [ "linger-users.service" ]; 446 after = 447 lib.optionals (cfg.backend == "docker") [ 448 "docker.service" 449 "docker.socket" 450 ] 451 # if imageFile or imageStream is not set, the service needs the network to download the image from the registry 452 ++ lib.optionals (container.imageFile == null && container.imageStream == null) [ 453 "network-online.target" 454 ] 455 ++ dependsOn 456 ++ lib.optionals dependOnLingerService [ "linger-users.service" ] 457 ++ lib.optionals (effectiveUser != "root" && container.podman.sdnotify == "healthy") [ 458 "user@${toString uid}.service" 459 ]; 460 requires = 461 dependsOn 462 ++ lib.optionals (effectiveUser != "root" && container.podman.sdnotify == "healthy") [ 463 "user@${toString uid}.service" 464 ]; 465 environment = lib.mkMerge [ 466 proxy_env 467 (mkIf (cfg.backend == "podman" && container.podman.user != "root") { 468 HOME = config.users.users.${container.podman.user}.home; 469 }) 470 ]; 471 472 path = 473 if cfg.backend == "docker" then 474 [ config.virtualisation.docker.package ] 475 else if cfg.backend == "podman" then 476 [ config.virtualisation.podman.package ] 477 else 478 throw "Unhandled backend: ${cfg.backend}"; 479 480 script = concatStringsSep " \\\n " ( 481 [ 482 "exec ${cfg.backend} " 483 ] 484 ++ map escapeShellArg container.preRunExtraOptions 485 ++ [ 486 "run" 487 "--name=${escapedName}" 488 "--log-driver=${container.log-driver}" 489 ] 490 ++ optional (container.entrypoint != null) "--entrypoint=${escapeShellArg container.entrypoint}" 491 ++ optional (container.hostname != null) "--hostname=${escapeShellArg container.hostname}" 492 ++ lib.optionals (cfg.backend == "podman") [ 493 "--cidfile=/run/${escapedName}/ctr-id" 494 "--cgroups=enabled" 495 "--sdnotify=${container.podman.sdnotify}" 496 "-d" 497 "--replace" 498 ] 499 ++ (mapAttrsToList (k: v: "-e ${escapeShellArg k}=${escapeShellArg v}") container.environment) 500 ++ map (f: "--env-file ${escapeShellArg f}") container.environmentFiles 501 ++ map (p: "-p ${escapeShellArg p}") container.ports 502 ++ optional (container.user != null) "-u ${escapeShellArg container.user}" 503 ++ map (v: "-v ${escapeShellArg v}") container.volumes 504 ++ (mapAttrsToList (k: v: "-l ${escapeShellArg k}=${escapeShellArg v}") container.labels) 505 ++ optional (container.workdir != null) "-w ${escapeShellArg container.workdir}" 506 ++ optional (container.privileged) "--privileged" 507 ++ optional (container.autoRemoveOnStop) "--rm" 508 ++ mapAttrsToList (k: _: "--cap-add=${escapeShellArg k}") ( 509 filterAttrs (_: v: v == true) container.capabilities 510 ) 511 ++ mapAttrsToList (k: _: "--cap-drop=${escapeShellArg k}") ( 512 filterAttrs (_: v: v == false) container.capabilities 513 ) 514 ++ map (d: "--device=${escapeShellArg d}") container.devices 515 ++ map (n: "--network=${escapeShellArg n}") container.networks 516 ++ [ "--pull ${escapeShellArg container.pull}" ] 517 ++ map escapeShellArg container.extraOptions 518 ++ [ container.image ] 519 ++ map escapeShellArg container.cmd 520 ); 521 522 preStop = 523 if cfg.backend == "podman" then 524 "podman stop --ignore --cidfile=/run/${escapedName}/ctr-id" 525 else 526 "${cfg.backend} stop ${name} || true"; 527 528 postStop = 529 if cfg.backend == "podman" then 530 "podman rm -f --ignore --cidfile=/run/${escapedName}/ctr-id" 531 else 532 "${cfg.backend} rm -f ${name} || true"; 533 534 unitConfig = mkIf (effectiveUser != "root") { 535 RequiresMountsFor = "/run/user/${toString uid}/containers"; 536 }; 537 538 serviceConfig = 539 { 540 ### There is no generalized way of supporting `reload` for docker 541 ### containers. Some containers may respond well to SIGHUP sent to their 542 ### init process, but it is not guaranteed; some apps have other reload 543 ### mechanisms, some don't have a reload signal at all, and some docker 544 ### images just have broken signal handling. The best compromise in this 545 ### case is probably to leave ExecReload undefined, so `systemctl reload` 546 ### will at least result in an error instead of potentially undefined 547 ### behaviour. 548 ### 549 ### Advanced users can still override this part of the unit to implement 550 ### a custom reload handler, since the result of all this is a normal 551 ### systemd service from the perspective of the NixOS module system. 552 ### 553 # ExecReload = ...; 554 ### 555 ExecStartPre = [ "${preStartScript}/bin/pre-start" ]; 556 TimeoutStartSec = 0; 557 TimeoutStopSec = 120; 558 Restart = "always"; 559 } 560 // optionalAttrs (cfg.backend == "podman") { 561 Environment = "PODMAN_SYSTEMD_UNIT=podman-${name}.service"; 562 Type = "notify"; 563 NotifyAccess = "all"; 564 Delegate = mkIf (container.podman.sdnotify == "healthy") true; 565 User = effectiveUser; 566 RuntimeDirectory = escapedName; 567 }; 568 }; 569 570in 571{ 572 imports = [ 573 (lib.mkChangedOptionModule [ "docker-containers" ] [ "virtualisation" "oci-containers" ] (oldcfg: { 574 backend = "docker"; 575 containers = lib.mapAttrs ( 576 n: v: 577 builtins.removeAttrs ( 578 v 579 // { 580 extraOptions = v.extraDockerOptions or [ ]; 581 } 582 ) [ "extraDockerOptions" ] 583 ) oldcfg.docker-containers; 584 })) 585 ]; 586 587 options.virtualisation.oci-containers = { 588 589 backend = mkOption { 590 type = types.enum [ 591 "podman" 592 "docker" 593 ]; 594 default = if versionAtLeast config.system.stateVersion "22.05" then "podman" else "docker"; 595 description = "The underlying Docker implementation to use."; 596 }; 597 598 containers = mkOption { 599 default = { }; 600 type = types.attrsOf (types.submodule containerOptions); 601 description = "OCI (Docker) containers to run as systemd services."; 602 }; 603 604 }; 605 606 config = lib.mkIf (cfg.containers != { }) ( 607 lib.mkMerge [ 608 { 609 systemd.services = mapAttrs' (n: v: nameValuePair v.serviceName (mkService n v)) cfg.containers; 610 611 assertions = 612 let 613 toAssertions = 614 name: 615 { 616 imageFile, 617 imageStream, 618 podman, 619 ... 620 }: 621 [ 622 { 623 assertion = imageFile == null || imageStream == null; 624 625 message = "virtualisation.oci-containers.containers.${name}: You can only define one of imageFile and imageStream"; 626 } 627 { 628 assertion = cfg.backend == "docker" -> podman == null; 629 message = "virtualisation.oci-containers.containers.${name}: Cannot set `podman` option if backend is `docker`."; 630 } 631 { 632 assertion = 633 cfg.backend == "podman" && podman.sdnotify == "healthy" && podman.user != "root" 634 -> config.users.users.${podman.user}.uid != null; 635 message = '' 636 Rootless container ${name} (with podman and sdnotify=healthy) 637 requires that its running user ${podman.user} has a statically specified uid. 638 ''; 639 } 640 ]; 641 in 642 concatMap (name: toAssertions name cfg.containers.${name}) (lib.attrNames cfg.containers); 643 644 warnings = mkIf (cfg.backend == "podman") ( 645 lib.foldlAttrs ( 646 warnings: name: 647 { podman, ... }: 648 let 649 inherit (config.users.users.${podman.user}) linger; 650 in 651 warnings 652 ++ lib.optional (podman.user != "root" && linger && podman.sdnotify == "conmon") '' 653 Podman container ${name} is configured as rootless (user ${podman.user}) 654 with `--sdnotify=conmon`, but lingering for this user is turned on. 655 '' 656 ++ lib.optional (podman.user != "root" && !linger && podman.sdnotify == "healthy") '' 657 Podman container ${name} is configured as rootless (user ${podman.user}) 658 with `--sdnotify=healthy`, but lingering for this user is turned off. 659 '' 660 ) [ ] cfg.containers 661 ); 662 } 663 (lib.mkIf (cfg.backend == "podman") { 664 virtualisation.podman.enable = true; 665 }) 666 (lib.mkIf (cfg.backend == "docker") { 667 virtualisation.docker.enable = true; 668 }) 669 ] 670 ); 671}