1 { config, options, lib, pkgs, ... }:
5 cfg = config.virtualisation.oci-containers;
6 proxy_env = config.networking.proxy.envVars;
8 defaultBackend = options.virtualisation.oci-containers.backend.default;
16 type = with types; str;
17 description = "OCI image to run.";
18 example = "library/hello-world";
21 imageFile = mkOption {
22 type = with types; nullOr package;
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.
33 example = literalExpression "pkgs.dockerTools.buildImage {...};";
36 imageStream = mkOption {
37 type = with types; nullOr package;
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
52 example = literalExpression "pkgs.dockerTools.streamLayeredImage {...};";
58 type = with types; nullOr str;
60 description = "Username for login.";
63 passwordFile = mkOption {
64 type = with types; nullOr str;
66 description = "Path to file containing password.";
67 example = "/etc/nixos/dockerhub-password.txt";
71 type = with types; nullOr str;
73 description = "Registry where to login to.";
74 example = "https://docker.pkg.github.com";
80 type = with types; listOf str;
82 description = "Commandline arguments to pass to the image's entrypoint.";
83 example = literalExpression ''
89 type = with types; attrsOf str;
91 description = "Labels to attach to the container at runtime.";
92 example = literalExpression ''
94 "traefik.https.routers.example.rule" = "Host(`example.container`)";
99 entrypoint = mkOption {
100 type = with types; nullOr str;
101 description = "Override the default entrypoint of the image.";
103 example = "/bin/my-app";
106 environment = mkOption {
107 type = with types; attrsOf str;
109 description = "Environment variables to set for this container.";
110 example = literalExpression ''
112 DATABASE_HOST = "db.example.com";
113 DATABASE_PORT = "3306";
118 environmentFiles = mkOption {
119 type = with types; listOf path;
121 description = "Environment files for this container.";
122 example = literalExpression ''
130 log-driver = mkOption {
132 default = "journald";
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.
141 [Docker engine documentation](https://docs.docker.com/engine/logging/configure/)
144 Refer to the docker-run(1) man page.
149 type = with types; listOf str;
152 Network ports to publish from the container to the outer host.
155 - `<ip>:<hostPort>:<containerPort>`
156 - `<ip>::<containerPort>`
157 - `<hostPort>:<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
173 Example: `127.0.0.1:1234:1234`
176 [Docker engine documentation](https://docs.docker.com/engine/network/#published-ports) for full details.
178 example = literalExpression ''
180 "127.0.0.1:8080:9000"
186 type = with types; nullOr str;
189 Override the username or UID (and optionally groupname or GID) used
192 example = "nobody:nogroup";
196 type = with types; listOf str;
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.
208 example = literalExpression ''
210 "volume_name:/path/inside/container"
211 "/path/on/host:/path/inside/container"
217 type = with types; nullOr str;
219 description = "Override the default working directory for the container.";
220 example = "/var/lib/hello_world";
223 dependsOn = mkOption {
224 type = with types; listOf str;
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`.
231 example = literalExpression ''
232 virtualisation.oci-containers.containers = {
235 dependsOn = [ "node1" ];
241 hostname = mkOption {
242 type = with types; nullOr str;
244 description = "The hostname of the container.";
245 example = "hello-world";
248 preRunExtraOptions = mkOption {
249 type = with types; listOf str;
251 description = "Extra options for {command}`${defaultBackend}` that go before the `run` argument.";
252 example = [ "--runtime" "runsc" ];
255 extraOptions = mkOption {
256 type = with types; listOf str;
258 description = "Extra options for {command}`${defaultBackend} run`.";
259 example = literalExpression ''
264 autoStart = mkOption {
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.
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 {
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; }
294 ${optionalString (container.imageFile != null) ''
295 ${cfg.backend} load -i ${container.imageFile}
297 ${optionalString (container.imageStream != null) ''
298 ${container.imageStream} | ${cfg.backend} load
300 ${optionalString (cfg.backend == "podman") ''
301 rm -f /run/podman-${escapedName}.ctr-id
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" ]
312 requires = dependsOn;
313 environment = proxy_env;
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 ++ [
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"
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
346 ++ map escapeShellArg container.cmd
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";
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
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.
373 ExecStartPre = [ "${preStartScript}/bin/pre-start" ];
375 TimeoutStopSec = 120;
377 } // optionalAttrs (cfg.backend == "podman") {
378 Environment="PODMAN_SYSTEMD_UNIT=podman-${name}.service";
387 lib.mkChangedOptionModule
388 [ "docker-containers" ]
389 [ "virtualisation" "oci-containers" ]
392 containers = lib.mapAttrs (n: v: builtins.removeAttrs (v // {
393 extraOptions = v.extraDockerOptions or [];
394 }) [ "extraDockerOptions" ]) oldcfg.docker-containers;
399 options.virtualisation.oci-containers = {
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.";
407 containers = mkOption {
409 type = types.attrsOf (types.submodule containerOptions);
410 description = "OCI (Docker) containers to run as systemd services.";
415 config = lib.mkIf (cfg.containers != {}) (lib.mkMerge [
417 systemd.services = mapAttrs' (n: v: nameValuePair "${cfg.backend}-${n}" (mkService n v)) cfg.containers;
421 toAssertion = _: { imageFile, imageStream, ... }:
422 { assertion = imageFile == null || imageStream == null;
424 message = "You can only define one of imageFile and imageStream";
428 lib.mapAttrsToList toAssertion cfg.containers;
430 (lib.mkIf (cfg.backend == "podman") {
431 virtualisation.podman.enable = true;
433 (lib.mkIf (cfg.backend == "docker") {
434 virtualisation.docker.enable = true;