system/machines/modules/docker-stack.nix

245 lines
7.2 KiB
Nix
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{ 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 "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 = mkOption {
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 doesnt 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;
};
};
}