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