grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / virtualisation / oci-containers.nix
blob161b4f6027b2f627460c9576a705d6a750736a23
1 { config, options, lib, pkgs, ... }:
3 with lib;
4 let
5   cfg = config.virtualisation.oci-containers;
6   proxy_env = config.networking.proxy.envVars;
8   defaultBackend = options.virtualisation.oci-containers.backend.default;
10   containerOptions =
11     { ... }: {
13       options = {
15         image = mkOption {
16           type = with types; str;
17           description = "OCI image to run.";
18           example = "library/hello-world";
19         };
21         imageFile = mkOption {
22           type = with types; nullOr package;
23           default = null;
24           description = ''
25             Path to an image file to load before running the image. This can
26             be used to bypass pulling the image from the registry.
28             The `image` attribute must match the name and
29             tag of the image contained in this file, as they will be used to
30             run the container with that image. If they do not match, the
31             image will be pulled from the registry as usual.
32           '';
33           example = literalExpression "pkgs.dockerTools.buildImage {...};";
34         };
36         imageStream = mkOption {
37           type = with types; nullOr package;
38           default = null;
39           description = ''
40             Path to a script that streams the desired image on standard output.
42             This option is mainly intended for use with
43             `pkgs.dockerTools.streamLayeredImage` so that the intermediate
44             image archive does not need to be stored in the Nix store.  For
45             larger images this optimization can significantly reduce Nix store
46             churn compared to using the `imageFile` option, because you don't
47             have to store a new copy of the image archive in the Nix store
48             every time you change the image.  Instead, if you stream the image
49             then you only need to build and store the layers that differ from
50             the previous image.
51           '';
52           example = literalExpression "pkgs.dockerTools.streamLayeredImage {...};";
53         };
55         login = {
57           username = mkOption {
58             type = with types; nullOr str;
59             default = null;
60             description = "Username for login.";
61           };
63           passwordFile = mkOption {
64             type = with types; nullOr str;
65             default = null;
66             description = "Path to file containing password.";
67             example = "/etc/nixos/dockerhub-password.txt";
68           };
70           registry = mkOption {
71             type = with types; nullOr str;
72             default = null;
73             description = "Registry where to login to.";
74             example = "https://docker.pkg.github.com";
75           };
77         };
79         cmd = mkOption {
80           type =  with types; listOf str;
81           default = [];
82           description = "Commandline arguments to pass to the image's entrypoint.";
83           example = literalExpression ''
84             ["--port=9000"]
85           '';
86         };
88         labels = mkOption {
89           type = with types; attrsOf str;
90           default = {};
91           description = "Labels to attach to the container at runtime.";
92           example = literalExpression ''
93             {
94               "traefik.https.routers.example.rule" = "Host(`example.container`)";
95             }
96           '';
97         };
99         entrypoint = mkOption {
100           type = with types; nullOr str;
101           description = "Override the default entrypoint of the image.";
102           default = null;
103           example = "/bin/my-app";
104         };
106         environment = mkOption {
107           type = with types; attrsOf str;
108           default = {};
109           description = "Environment variables to set for this container.";
110           example = literalExpression ''
111             {
112               DATABASE_HOST = "db.example.com";
113               DATABASE_PORT = "3306";
114             }
115         '';
116         };
118         environmentFiles = mkOption {
119           type = with types; listOf path;
120           default = [];
121           description = "Environment files for this container.";
122           example = literalExpression ''
123             [
124               /path/to/.env
125               /path/to/.env.secret
126             ]
127         '';
128         };
130         log-driver = mkOption {
131           type = types.str;
132           default = "journald";
133           description = ''
134             Logging driver for the container.  The default of
135             `"journald"` means that the container's logs will be
136             handled as part of the systemd unit.
138             For more details and a full list of logging drivers, refer to respective backends documentation.
140             For Docker:
141             [Docker engine documentation](https://docs.docker.com/engine/logging/configure/)
143             For Podman:
144             Refer to the docker-run(1) man page.
145           '';
146         };
148         ports = mkOption {
149           type = with types; listOf str;
150           default = [];
151           description = ''
152             Network ports to publish from the container to the outer host.
154             Valid formats:
155             - `<ip>:<hostPort>:<containerPort>`
156             - `<ip>::<containerPort>`
157             - `<hostPort>:<containerPort>`
158             - `<containerPort>`
160             Both `hostPort` and `containerPort` can be specified as a range of
161             ports.  When specifying ranges for both, the number of container
162             ports in the range must match the number of host ports in the
163             range.  Example: `1234-1236:1234-1236/tcp`
165             When specifying a range for `hostPort` only, the `containerPort`
166             must *not* be a range.  In this case, the container port is published
167             somewhere within the specified `hostPort` range.
168             Example: `1234-1236:1234/tcp`
170             Publishing a port bypasses the NixOS firewall. If the port is not
171             supposed to be shared on the network, make sure to publish the
172             port to localhost.
173             Example: `127.0.0.1:1234:1234`
175             Refer to the
176             [Docker engine documentation](https://docs.docker.com/engine/network/#published-ports) for full details.
177           '';
178           example = literalExpression ''
179             [
180               "127.0.0.1:8080:9000"
181             ]
182           '';
183         };
185         user = mkOption {
186           type = with types; nullOr str;
187           default = null;
188           description = ''
189             Override the username or UID (and optionally groupname or GID) used
190             in the container.
191           '';
192           example = "nobody:nogroup";
193         };
195         volumes = mkOption {
196           type = with types; listOf str;
197           default = [];
198           description = ''
199             List of volumes to attach to this container.
201             Note that this is a list of `"src:dst"` strings to
202             allow for `src` to refer to `/nix/store` paths, which
203             would be difficult with an attribute set.  There are
204             also a variety of mount options available as a third
205             field; please refer to the
206             [docker engine documentation](https://docs.docker.com/engine/storage/volumes/) for details.
207           '';
208           example = literalExpression ''
209             [
210               "volume_name:/path/inside/container"
211               "/path/on/host:/path/inside/container"
212             ]
213           '';
214         };
216         workdir = mkOption {
217           type = with types; nullOr str;
218           default = null;
219           description = "Override the default working directory for the container.";
220           example = "/var/lib/hello_world";
221         };
223         dependsOn = mkOption {
224           type = with types; listOf str;
225           default = [];
226           description = ''
227             Define which other containers this one depends on. They will be added to both After and Requires for the unit.
229             Use the same name as the attribute under `virtualisation.oci-containers.containers`.
230           '';
231           example = literalExpression ''
232             virtualisation.oci-containers.containers = {
233               node1 = {};
234               node2 = {
235                 dependsOn = [ "node1" ];
236               }
237             }
238           '';
239         };
241         hostname = mkOption {
242           type = with types; nullOr str;
243           default = null;
244           description = "The hostname of the container.";
245           example = "hello-world";
246         };
248         preRunExtraOptions = mkOption {
249           type = with types; listOf str;
250           default = [];
251           description = "Extra options for {command}`${defaultBackend}` that go before the `run` argument.";
252           example = [ "--runtime" "runsc" ];
253         };
255         extraOptions = mkOption {
256           type = with types; listOf str;
257           default = [];
258           description = "Extra options for {command}`${defaultBackend} run`.";
259           example = literalExpression ''
260             ["--network=host"]
261           '';
262         };
264         autoStart = mkOption {
265           type = types.bool;
266           default = true;
267           description = ''
268             When enabled, the container is automatically started on boot.
269             If this option is set to false, the container has to be started on-demand via its service.
270           '';
271         };
272       };
273     };
275   isValidLogin = login: login.username != null && login.passwordFile != null && login.registry != null;
277   mkService = name: container: let
278     dependsOn = map (x: "${cfg.backend}-${x}.service") container.dependsOn;
279     escapedName = escapeShellArg name;
280     preStartScript = pkgs.writeShellApplication {
281       name = "pre-start";
282       runtimeInputs = [ ];
283       text = ''
284         ${cfg.backend} rm -f ${name} || true
285         ${optionalString (isValidLogin container.login) ''
286           # try logging in, if it fails, check if image exists locally
287           ${cfg.backend} login \
288           ${container.login.registry} \
289           --username ${container.login.username} \
290           --password-stdin < ${container.login.passwordFile} \
291           || ${cfg.backend} image inspect ${container.image} >/dev/null \
292           || { echo "image doesn't exist locally and login failed" >&2 ; exit 1; }
293         ''}
294         ${optionalString (container.imageFile != null) ''
295           ${cfg.backend} load -i ${container.imageFile}
296         ''}
297         ${optionalString (container.imageStream != null) ''
298           ${container.imageStream} | ${cfg.backend} load
299         ''}
300         ${optionalString (cfg.backend == "podman") ''
301           rm -f /run/podman-${escapedName}.ctr-id
302         ''}
303       '';
304     };
305   in {
306     wantedBy = [] ++ optional (container.autoStart) "multi-user.target";
307     wants = lib.optional (container.imageFile == null && container.imageStream == null)  "network-online.target";
308     after = lib.optionals (cfg.backend == "docker") [ "docker.service" "docker.socket" ]
309             # if imageFile or imageStream is not set, the service needs the network to download the image from the registry
310             ++ lib.optionals (container.imageFile == null && container.imageStream == null) [ "network-online.target" ]
311             ++ dependsOn;
312     requires = dependsOn;
313     environment = proxy_env;
315     path =
316       if cfg.backend == "docker" then [ config.virtualisation.docker.package ]
317       else if cfg.backend == "podman" then [ config.virtualisation.podman.package ]
318       else throw "Unhandled backend: ${cfg.backend}";
320     script = concatStringsSep " \\\n  " ([
321       "exec ${cfg.backend} "
322     ]  ++ map escapeShellArg container.preRunExtraOptions ++ [
323       "run"
324       "--rm"
325       "--name=${escapedName}"
326       "--log-driver=${container.log-driver}"
327     ] ++ optional (container.entrypoint != null)
328       "--entrypoint=${escapeShellArg container.entrypoint}"
329       ++ optional (container.hostname != null)
330       "--hostname=${escapeShellArg container.hostname}"
331       ++ lib.optionals (cfg.backend == "podman") [
332         "--cidfile=/run/podman-${escapedName}.ctr-id"
333         "--cgroups=no-conmon"
334         "--sdnotify=conmon"
335         "-d"
336         "--replace"
337       ] ++ (mapAttrsToList (k: v: "-e ${escapeShellArg k}=${escapeShellArg v}") container.environment)
338       ++ map (f: "--env-file ${escapeShellArg f}") container.environmentFiles
339       ++ map (p: "-p ${escapeShellArg p}") container.ports
340       ++ optional (container.user != null) "-u ${escapeShellArg container.user}"
341       ++ map (v: "-v ${escapeShellArg v}") container.volumes
342       ++ (mapAttrsToList (k: v: "-l ${escapeShellArg k}=${escapeShellArg v}") container.labels)
343       ++ optional (container.workdir != null) "-w ${escapeShellArg container.workdir}"
344       ++ map escapeShellArg container.extraOptions
345       ++ [container.image]
346       ++ map escapeShellArg container.cmd
347     );
349     preStop = if cfg.backend == "podman"
350       then "podman stop --ignore --cidfile=/run/podman-${escapedName}.ctr-id"
351       else "${cfg.backend} stop ${name} || true";
353     postStop = if cfg.backend == "podman"
354       then "podman rm -f --ignore --cidfile=/run/podman-${escapedName}.ctr-id"
355       else "${cfg.backend} rm -f ${name} || true";
357     serviceConfig = {
358       ### There is no generalized way of supporting `reload` for docker
359       ### containers. Some containers may respond well to SIGHUP sent to their
360       ### init process, but it is not guaranteed; some apps have other reload
361       ### mechanisms, some don't have a reload signal at all, and some docker
362       ### images just have broken signal handling.  The best compromise in this
363       ### case is probably to leave ExecReload undefined, so `systemctl reload`
364       ### will at least result in an error instead of potentially undefined
365       ### behaviour.
366       ###
367       ### Advanced users can still override this part of the unit to implement
368       ### a custom reload handler, since the result of all this is a normal
369       ### systemd service from the perspective of the NixOS module system.
370       ###
371       # ExecReload = ...;
372       ###
373       ExecStartPre = [ "${preStartScript}/bin/pre-start" ];
374       TimeoutStartSec = 0;
375       TimeoutStopSec = 120;
376       Restart = "always";
377     } // optionalAttrs (cfg.backend == "podman") {
378       Environment="PODMAN_SYSTEMD_UNIT=podman-${name}.service";
379       Type="notify";
380       NotifyAccess="all";
381     };
382   };
384 in {
385   imports = [
386     (
387       lib.mkChangedOptionModule
388       [ "docker-containers"  ]
389       [ "virtualisation" "oci-containers" ]
390       (oldcfg: {
391         backend = "docker";
392         containers = lib.mapAttrs (n: v: builtins.removeAttrs (v // {
393           extraOptions = v.extraDockerOptions or [];
394         }) [ "extraDockerOptions" ]) oldcfg.docker-containers;
395       })
396     )
397   ];
399   options.virtualisation.oci-containers = {
401     backend = mkOption {
402       type = types.enum [ "podman" "docker" ];
403       default = if versionAtLeast config.system.stateVersion "22.05" then "podman" else "docker";
404       description = "The underlying Docker implementation to use.";
405     };
407     containers = mkOption {
408       default = {};
409       type = types.attrsOf (types.submodule containerOptions);
410       description = "OCI (Docker) containers to run as systemd services.";
411     };
413   };
415   config = lib.mkIf (cfg.containers != {}) (lib.mkMerge [
416     {
417       systemd.services = mapAttrs' (n: v: nameValuePair "${cfg.backend}-${n}" (mkService n v)) cfg.containers;
419       assertions =
420         let
421           toAssertion = _: { imageFile, imageStream, ... }:
422             { assertion = imageFile == null || imageStream == null;
424               message = "You can only define one of imageFile and imageStream";
425             };
427         in
428           lib.mapAttrsToList toAssertion cfg.containers;
429     }
430     (lib.mkIf (cfg.backend == "podman") {
431       virtualisation.podman.enable = true;
432     })
433     (lib.mkIf (cfg.backend == "docker") {
434       virtualisation.docker.enable = true;
435     })
436   ]);