{ 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 "Networks to join."; example = literalExpression '' [ "backend_internal" "traefik_public" ] ''; }; extra_hosts = mkOption { type = types.listOf types.str; default = [ ]; description = lib.mdDoc "Add hostname mappings."; example = literalExpression '' [ "host.docker.internal:host-gateway" "otherhost:50.31.209.229" ] ''; }; deploy = { labels = { default = [ ]; type = types.listOf types.str; description = lib.mdDoc "Specify labels for the service."; example = literalExpression '' [ "com.example.description=This label will appear on the web service" ] ''; }; placement = { constraints = mkOption { default = [ ]; type = types.listOf types.str; description = lib.mdDoc '' You can limit the set of nodes where a task can be scheduled by defining constraint expressions. Constraint expressions can either use a match (==) or exclude (!=) rule. Multiple constraints find nodes that satisfy every expression (AND match). ''; example = literalExample '' [ "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 = false; type = types.nullOr types.bool; description = lib.mdDoc '' If set to true, specifies that this volume has been created outside of Compose. The systemd service does not attempt to create it, and raises an error if it doesn’t exist. ''; 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 ''; mkStackTimer = stackName: { wantedBy = [ "timers.target" ]; timerConfig = { OnBootSec = "5m"; OnUnitActiveSec = "5m"; Unit = "docker-stack-${stackName}.service"; }; }; mkStackService = stackName: stack: let escapedStackName = lib.escapeShellArg stackName; composeFile = mkComposeFile stack; in { description = "Deploy ${escapedStackName} stack"; enable = true; 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.timers = lib.mapAttrs' (n: v: lib.nameValuePair "docker-stack-${n}" (mkStackTimer n)) cfg.stacks; systemd.services = lib.mapAttrs' (n: v: lib.nameValuePair "docker-stack-${n}" (mkStackService n v)) cfg.stacks; virtualisation.docker = { enable = true; liveRestore = false; }; }; }