system/machines/modules/docker-stack.nix

224 lines
6.2 KiB
Nix

{ 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:
- `<ip>:<hostPort>:<containerPort>`
- `<ip>::<containerPort>`
- `<hostPort>:<containerPort>`
- `<containerPort>`
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
'';
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.mkIfattrNames (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;
};
};
}