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