{ config, lib, pkgs, ... }: let cfg = config.virtualisation.docker; proxyEnv = config.networking.proxy.envVars; serviceOptions = { ... }: { options = with lib; { image = mkOption { type = types.str; description = lib.mdDoc "OCI image to run."; example = "library/hello-world"; }; command = mkOption { type = types.listOf types.str; default = [ ]; description = lib.mdDoc "Overrides the default command declared by the image (i.e. by Dockerfile's CMD)."; example = literalExpression '' [ "--port=9000" ] ''; }; ports = mkOption { type = types.listOf types.str; default = [ ]; description = lib.mdDoc '' Network ports to publish from the container to the outer host. Valid formats: - `::` - `::` - `:` - `` Both `hostPort` and `containerPort` can be specified as a range of ports. When specifying ranges for both, the number of container ports in the range must match the number of host ports in the range. Example: `1234-1236:1234-1236/tcp` When specifying a range for `hostPort` only, the `containerPort` must *not* be a range. In this case, the container port is published somewhere within the specified `hostPort` range. Example: `1234-1236:1234/tcp` Refer to the [Docker engine documentation](https://docs.docker.com/engine/reference/run/#expose-incoming-ports) for full details. ''; example = literalExpression '' [ "8080:9000" ] ''; }; volumes = mkOption { type = types.listOf types.str; default = [ ]; description = lib.mdDoc '' List of volumes to attach to this container. Note that this is a list of `"src:dst"` strings to allow for `src` to refer to `/nix/store` paths, which would be difficult with an attribute set. There are also a variety of mount options available as a third field; please refer to the [docker engine documentation](https://docs.docker.com/engine/reference/run/#volume-shared-filesystems) for details. ''; example = literalExpression '' [ "volume_name:/path/inside/container" "/path/on/host:/path/inside/container" ] ''; }; networks = mkOption { type = types.listOf types.str; default = [ ]; description = lib.mdDoc '' ''; example = literalExpression '' [ "backend_internal" "traefik_public" ] ''; }; extra_hosts = mkOption { type = types.listOf types.str; default = [ ]; description = lib.mdDoc '' ''; example = literalExpression '' [ "host.docker.internal:host-gateway" ] ''; }; deploy = { placement = { constraints = mkOption { default = [ ]; type = types.listOf types.str; description = lib.mdDoc ""; example = [ "node.role==manager" ]; }; }; update_config = { order = mkOption { default = "stop-first"; type = types.str; description = lib.mdDoc '' Order of operations during updates. - stop-first (old task is stopped before starting new one), - start-first (new task is started first, and the running tasks briefly overlap) Note: Only supported for v3.4 and higher. ''; example = "start-first"; }; }; }; }; }; networkOptions = { ... }: { options = with lib; { external = mkOption { default = null; type = types.nullOr types.bool; description = lib.mdDoc ""; example = "true"; }; }; }; stackOptions = { ... }: { options = with lib; { version = mkOption { type = types.str; default = "3.8"; }; services = mkOption { default = { }; type = types.attrsOf (types.submodule serviceOptions); description = lib.mdDoc ""; }; networks = mkOption { default = { }; type = types.attrsOf (types.submodule networkOptions); description = lib.mdDoc ""; }; }; }; mkComposeFile = stack: pkgs.runCommand "compose.yml" { buildInputs = [ pkgs.remarshal ]; preferLocalBuild = true; } '' remarshal -if json -of yaml \ < ${ pkgs.writeText "compose.json" (builtins.toJSON stack)} \ > $out ''; mkStackService = stackName: stack: let escapedStackName = lib.escapeShellArg stackName; composeFile = mkComposeFile stack; in { description = "Deploy ${escapedStackName} stack"; after = [ "docker.service" "docker.socket" ]; environment = proxyEnv; path = [ config.virtualisation.docker.package ]; script = lib.concatStringsSep " \\\n " ([ "exec docker stack deploy" "--compose-file=${composeFile}" escapedStackName ]); serviceConfig = { Type = "oneshot"; }; }; in { options.virtualisation.docker.stacks = with lib; mkOption { default = { }; type = types.attrsOf (types.submodule stackOptions); description = lib.mdDoc "Docker stacks to deploy using systemd services."; }; config = lib.mkIf (cfg.stacks != { }) { systemd.services = lib.mapAttrs' (n: v: lib.nameValuePair "docker-stack-${n}" (mkStackService n v)) cfg.stacks; virtualisation.docker = { enable = true; liveRestore = false; }; }; }