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