at 24.11-pre 14 kB view raw
1{ config, options, lib, pkgs, ... }: 2 3with lib; 4let 5 cfg = config.virtualisation.oci-containers; 6 proxy_env = config.networking.proxy.envVars; 7 8 defaultBackend = options.virtualisation.oci-containers.backend.default; 9 10 containerOptions = 11 { ... }: { 12 13 options = { 14 15 image = mkOption { 16 type = with types; str; 17 description = "OCI image to run."; 18 example = "library/hello-world"; 19 }; 20 21 imageFile = mkOption { 22 type = with types; nullOr package; 23 default = null; 24 description = '' 25 Path to an image file to load before running the image. This can 26 be used to bypass pulling the image from the registry. 27 28 The `image` attribute must match the name and 29 tag of the image contained in this file, as they will be used to 30 run the container with that image. If they do not match, the 31 image will be pulled from the registry as usual. 32 ''; 33 example = literalExpression "pkgs.dockerTools.buildImage {...};"; 34 }; 35 36 login = { 37 38 username = mkOption { 39 type = with types; nullOr str; 40 default = null; 41 description = "Username for login."; 42 }; 43 44 passwordFile = mkOption { 45 type = with types; nullOr str; 46 default = null; 47 description = "Path to file containing password."; 48 example = "/etc/nixos/dockerhub-password.txt"; 49 }; 50 51 registry = mkOption { 52 type = with types; nullOr str; 53 default = null; 54 description = "Registry where to login to."; 55 example = "https://docker.pkg.github.com"; 56 }; 57 58 }; 59 60 cmd = mkOption { 61 type = with types; listOf str; 62 default = []; 63 description = "Commandline arguments to pass to the image's entrypoint."; 64 example = literalExpression '' 65 ["--port=9000"] 66 ''; 67 }; 68 69 labels = mkOption { 70 type = with types; attrsOf str; 71 default = {}; 72 description = "Labels to attach to the container at runtime."; 73 example = literalExpression '' 74 { 75 "traefik.https.routers.example.rule" = "Host(`example.container`)"; 76 } 77 ''; 78 }; 79 80 entrypoint = mkOption { 81 type = with types; nullOr str; 82 description = "Override the default entrypoint of the image."; 83 default = null; 84 example = "/bin/my-app"; 85 }; 86 87 environment = mkOption { 88 type = with types; attrsOf str; 89 default = {}; 90 description = "Environment variables to set for this container."; 91 example = literalExpression '' 92 { 93 DATABASE_HOST = "db.example.com"; 94 DATABASE_PORT = "3306"; 95 } 96 ''; 97 }; 98 99 environmentFiles = mkOption { 100 type = with types; listOf path; 101 default = []; 102 description = "Environment files for this container."; 103 example = literalExpression '' 104 [ 105 /path/to/.env 106 /path/to/.env.secret 107 ] 108 ''; 109 }; 110 111 log-driver = mkOption { 112 type = types.str; 113 default = "journald"; 114 description = '' 115 Logging driver for the container. The default of 116 `"journald"` means that the container's logs will be 117 handled as part of the systemd unit. 118 119 For more details and a full list of logging drivers, refer to respective backends documentation. 120 121 For Docker: 122 [Docker engine documentation](https://docs.docker.com/engine/reference/run/#logging-drivers---log-driver) 123 124 For Podman: 125 Refer to the docker-run(1) man page. 126 ''; 127 }; 128 129 ports = mkOption { 130 type = with types; listOf str; 131 default = []; 132 description = '' 133 Network ports to publish from the container to the outer host. 134 135 Valid formats: 136 - `<ip>:<hostPort>:<containerPort>` 137 - `<ip>::<containerPort>` 138 - `<hostPort>:<containerPort>` 139 - `<containerPort>` 140 141 Both `hostPort` and `containerPort` can be specified as a range of 142 ports. When specifying ranges for both, the number of container 143 ports in the range must match the number of host ports in the 144 range. Example: `1234-1236:1234-1236/tcp` 145 146 When specifying a range for `hostPort` only, the `containerPort` 147 must *not* be a range. In this case, the container port is published 148 somewhere within the specified `hostPort` range. 149 Example: `1234-1236:1234/tcp` 150 151 Refer to the 152 [Docker engine documentation](https://docs.docker.com/engine/reference/run/#expose-incoming-ports) for full details. 153 ''; 154 example = literalExpression '' 155 [ 156 "8080:9000" 157 ] 158 ''; 159 }; 160 161 user = mkOption { 162 type = with types; nullOr str; 163 default = null; 164 description = '' 165 Override the username or UID (and optionally groupname or GID) used 166 in the container. 167 ''; 168 example = "nobody:nogroup"; 169 }; 170 171 volumes = mkOption { 172 type = with types; listOf str; 173 default = []; 174 description = '' 175 List of volumes to attach to this container. 176 177 Note that this is a list of `"src:dst"` strings to 178 allow for `src` to refer to `/nix/store` paths, which 179 would be difficult with an attribute set. There are 180 also a variety of mount options available as a third 181 field; please refer to the 182 [docker engine documentation](https://docs.docker.com/engine/reference/run/#volume-shared-filesystems) for details. 183 ''; 184 example = literalExpression '' 185 [ 186 "volume_name:/path/inside/container" 187 "/path/on/host:/path/inside/container" 188 ] 189 ''; 190 }; 191 192 workdir = mkOption { 193 type = with types; nullOr str; 194 default = null; 195 description = "Override the default working directory for the container."; 196 example = "/var/lib/hello_world"; 197 }; 198 199 dependsOn = mkOption { 200 type = with types; listOf str; 201 default = []; 202 description = '' 203 Define which other containers this one depends on. They will be added to both After and Requires for the unit. 204 205 Use the same name as the attribute under `virtualisation.oci-containers.containers`. 206 ''; 207 example = literalExpression '' 208 virtualisation.oci-containers.containers = { 209 node1 = {}; 210 node2 = { 211 dependsOn = [ "node1" ]; 212 } 213 } 214 ''; 215 }; 216 217 hostname = mkOption { 218 type = with types; nullOr str; 219 default = null; 220 description = "The hostname of the container."; 221 example = "hello-world"; 222 }; 223 224 extraOptions = mkOption { 225 type = with types; listOf str; 226 default = []; 227 description = "Extra options for {command}`${defaultBackend} run`."; 228 example = literalExpression '' 229 ["--network=host"] 230 ''; 231 }; 232 233 autoStart = mkOption { 234 type = types.bool; 235 default = true; 236 description = '' 237 When enabled, the container is automatically started on boot. 238 If this option is set to false, the container has to be started on-demand via its service. 239 ''; 240 }; 241 }; 242 }; 243 244 isValidLogin = login: login.username != null && login.passwordFile != null && login.registry != null; 245 246 mkService = name: container: let 247 dependsOn = map (x: "${cfg.backend}-${x}.service") container.dependsOn; 248 escapedName = escapeShellArg name; 249 preStartScript = pkgs.writeShellApplication { 250 name = "pre-start"; 251 runtimeInputs = [ ]; 252 text = '' 253 ${cfg.backend} rm -f ${name} || true 254 ${optionalString (isValidLogin container.login) '' 255 # try logging in, if it fails, check if image exists locally 256 ${cfg.backend} login \ 257 ${container.login.registry} \ 258 --username ${container.login.username} \ 259 --password-stdin < ${container.login.passwordFile} \ 260 || ${cfg.backend} image inspect ${container.image} >/dev/null \ 261 || { echo "image doesn't exist locally and login failed" >&2 ; exit 1; } 262 ''} 263 ${optionalString (container.imageFile != null) '' 264 ${cfg.backend} load -i ${container.imageFile} 265 ''} 266 ${optionalString (cfg.backend == "podman") '' 267 rm -f /run/podman-${escapedName}.ctr-id 268 ''} 269 ''; 270 }; 271 in { 272 wantedBy = [] ++ optional (container.autoStart) "multi-user.target"; 273 wants = lib.optional (container.imageFile == null) "network-online.target"; 274 after = lib.optionals (cfg.backend == "docker") [ "docker.service" "docker.socket" ] 275 # if imageFile is not set, the service needs the network to download the image from the registry 276 ++ lib.optionals (container.imageFile == null) [ "network-online.target" ] 277 ++ dependsOn; 278 requires = dependsOn; 279 environment = proxy_env; 280 281 path = 282 if cfg.backend == "docker" then [ config.virtualisation.docker.package ] 283 else if cfg.backend == "podman" then [ config.virtualisation.podman.package ] 284 else throw "Unhandled backend: ${cfg.backend}"; 285 286 script = concatStringsSep " \\\n " ([ 287 "exec ${cfg.backend} run" 288 "--rm" 289 "--name=${escapedName}" 290 "--log-driver=${container.log-driver}" 291 ] ++ optional (container.entrypoint != null) 292 "--entrypoint=${escapeShellArg container.entrypoint}" 293 ++ optional (container.hostname != null) 294 "--hostname=${escapeShellArg container.hostname}" 295 ++ lib.optionals (cfg.backend == "podman") [ 296 "--cidfile=/run/podman-${escapedName}.ctr-id" 297 "--cgroups=no-conmon" 298 "--sdnotify=conmon" 299 "-d" 300 "--replace" 301 ] ++ (mapAttrsToList (k: v: "-e ${escapeShellArg k}=${escapeShellArg v}") container.environment) 302 ++ map (f: "--env-file ${escapeShellArg f}") container.environmentFiles 303 ++ map (p: "-p ${escapeShellArg p}") container.ports 304 ++ optional (container.user != null) "-u ${escapeShellArg container.user}" 305 ++ map (v: "-v ${escapeShellArg v}") container.volumes 306 ++ (mapAttrsToList (k: v: "-l ${escapeShellArg k}=${escapeShellArg v}") container.labels) 307 ++ optional (container.workdir != null) "-w ${escapeShellArg container.workdir}" 308 ++ map escapeShellArg container.extraOptions 309 ++ [container.image] 310 ++ map escapeShellArg container.cmd 311 ); 312 313 preStop = if cfg.backend == "podman" 314 then "podman stop --ignore --cidfile=/run/podman-${escapedName}.ctr-id" 315 else "${cfg.backend} stop ${name} || true"; 316 317 postStop = if cfg.backend == "podman" 318 then "podman rm -f --ignore --cidfile=/run/podman-${escapedName}.ctr-id" 319 else "${cfg.backend} rm -f ${name} || true"; 320 321 serviceConfig = { 322 ### There is no generalized way of supporting `reload` for docker 323 ### containers. Some containers may respond well to SIGHUP sent to their 324 ### init process, but it is not guaranteed; some apps have other reload 325 ### mechanisms, some don't have a reload signal at all, and some docker 326 ### images just have broken signal handling. The best compromise in this 327 ### case is probably to leave ExecReload undefined, so `systemctl reload` 328 ### will at least result in an error instead of potentially undefined 329 ### behaviour. 330 ### 331 ### Advanced users can still override this part of the unit to implement 332 ### a custom reload handler, since the result of all this is a normal 333 ### systemd service from the perspective of the NixOS module system. 334 ### 335 # ExecReload = ...; 336 ### 337 ExecStartPre = [ "${preStartScript}/bin/pre-start" ]; 338 TimeoutStartSec = 0; 339 TimeoutStopSec = 120; 340 Restart = "always"; 341 } // optionalAttrs (cfg.backend == "podman") { 342 Environment="PODMAN_SYSTEMD_UNIT=podman-${name}.service"; 343 Type="notify"; 344 NotifyAccess="all"; 345 }; 346 }; 347 348in { 349 imports = [ 350 ( 351 lib.mkChangedOptionModule 352 [ "docker-containers" ] 353 [ "virtualisation" "oci-containers" ] 354 (oldcfg: { 355 backend = "docker"; 356 containers = lib.mapAttrs (n: v: builtins.removeAttrs (v // { 357 extraOptions = v.extraDockerOptions or []; 358 }) [ "extraDockerOptions" ]) oldcfg.docker-containers; 359 }) 360 ) 361 ]; 362 363 options.virtualisation.oci-containers = { 364 365 backend = mkOption { 366 type = types.enum [ "podman" "docker" ]; 367 default = if versionAtLeast config.system.stateVersion "22.05" then "podman" else "docker"; 368 description = "The underlying Docker implementation to use."; 369 }; 370 371 containers = mkOption { 372 default = {}; 373 type = types.attrsOf (types.submodule containerOptions); 374 description = "OCI (Docker) containers to run as systemd services."; 375 }; 376 377 }; 378 379 config = lib.mkIf (cfg.containers != {}) (lib.mkMerge [ 380 { 381 systemd.services = mapAttrs' (n: v: nameValuePair "${cfg.backend}-${n}" (mkService n v)) cfg.containers; 382 } 383 (lib.mkIf (cfg.backend == "podman") { 384 virtualisation.podman.enable = true; 385 }) 386 (lib.mkIf (cfg.backend == "docker") { 387 virtualisation.docker.enable = true; 388 }) 389 ]); 390 391}