diff --git a/configuration/services/gitea.nix b/configuration/services/gitea.nix index 0abdb49..b0f6d68 100644 --- a/configuration/services/gitea.nix +++ b/configuration/services/gitea.nix @@ -17,7 +17,7 @@ containers = { gitea = { - image = "gitea/gitea:latest"; + image = "docker.io/gitea/gitea:latest"; volumes = [ "gitea:/data:Z" "/etc/localtime:/etc/localtime:ro" ]; dependsOn = [ "postgres" ]; @@ -35,6 +35,10 @@ DOMAIN = "gitea.tlater.net"; SSH_PORT = "2221"; }; + + extraOptions = [ + "--label" "io.containers.autoupdate=image" + ]; }; postgres = { diff --git a/modules/default.nix b/modules/default.nix index 0bc1f1c..4b9cea4 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -1,5 +1,5 @@ { ... }: { - imports = [ ./virtualisation/pods.nix ]; + imports = [ ./virtualisation/pods.nix ./virtualisation/oci-containers.nix ]; } diff --git a/modules/virtualisation/oci-containers.nix b/modules/virtualisation/oci-containers.nix new file mode 100644 index 0000000..f37dcaf --- /dev/null +++ b/modules/virtualisation/oci-containers.nix @@ -0,0 +1,350 @@ +# Pulled from my own modified fork of nixpkgs, awaiting merge +# upstream. + +{ config, options, lib, pkgs, ... }: + +with lib; +let + cfg = config.virtualisation.oci-containers; + proxy_env = config.networking.proxy.envVars; + + defaultBackend = options.virtualisation.oci-containers.backend.default; + + containerOptions = + { ... }: { + + options = { + + image = mkOption { + type = with types; str; + description = "OCI image to run."; + example = "library/hello-world"; + }; + + imageFile = mkOption { + type = with types; nullOr package; + default = null; + description = '' + Path to an image file to load instead of pulling from a registry. + If defined, do not pull from registry. + + You still need to set the image attribute, as it + will be used as the image name for docker to start a container. + ''; + example = literalExample "pkgs.dockerTools.buildDockerImage {...};"; + }; + + cmd = mkOption { + type = with types; listOf str; + default = []; + description = "Commandline arguments to pass to the image's entrypoint."; + example = literalExample '' + ["--port=9000"] + ''; + }; + + entrypoint = mkOption { + type = with types; nullOr str; + description = "Override the default entrypoint of the image."; + default = null; + example = "/bin/my-app"; + }; + + environment = mkOption { + type = with types; attrsOf str; + default = {}; + description = "Environment variables to set for this container."; + example = literalExample '' + { + DATABASE_HOST = "db.example.com"; + DATABASE_PORT = "3306"; + } + ''; + }; + + environmentFiles = mkOption { + type = with types; listOf path; + default = []; + description = "Environment files for this container."; + example = literalExample '' + [ + /path/to/.env + /path/to/.env.secret + ] + ''; + }; + + log-driver = mkOption { + type = types.str; + default = "journald"; + description = '' + Logging driver for the container. The default of + "journald" means that the container's logs will be + handled as part of the systemd unit. + + For more details and a full list of logging drivers, refer to respective backends documentation. + + For Docker: + Docker engine documentation + + For Podman: + Refer to the docker-run(1) man page. + ''; + }; + + ports = mkOption { + type = with types; listOf str; + default = []; + description = '' + 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 for full details. + ''; + example = literalExample '' + [ + "8080:9000" + ] + ''; + }; + + user = mkOption { + type = with types; nullOr str; + default = null; + description = '' + Override the username or UID (and optionally groupname or GID) used + in the container. + ''; + example = "nobody:nogroup"; + }; + + volumes = mkOption { + type = with types; listOf str; + default = []; + description = '' + 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 for details. + ''; + example = literalExample '' + [ + "volume_name:/path/inside/container" + "/path/on/host:/path/inside/container" + ] + ''; + }; + + workdir = mkOption { + type = with types; nullOr str; + default = null; + description = "Override the default working directory for the container."; + example = "/var/lib/hello_world"; + }; + + dependsOn = mkOption { + type = with types; listOf str; + default = []; + description = '' + Define which other containers this one depends on. They will be added to both After and Requires for the unit. + + Use the same name as the attribute under virtualisation.oci-containers.containers. + ''; + example = literalExample '' + virtualisation.oci-containers.containers = { + node1 = {}; + node2 = { + dependsOn = [ "node1" ]; + } + } + ''; + }; + + extraOptions = mkOption { + type = with types; listOf str; + default = []; + description = "Extra options for ${defaultBackend} run."; + example = literalExample '' + ["--network=host"] + ''; + }; + + autoStart = mkOption { + type = types.bool; + default = true; + description = '' + When enabled, the container is automatically started on boot. + If this option is set to false, the container has to be started on-demand via its service. + ''; + }; + }; + }; + + mkService = name: container: let + dependsOn = map (x: "${cfg.backend}-${x}.service") container.dependsOn; + in { + wantedBy = [] ++ optional (container.autoStart) "multi-user.target"; + after = lib.optionals (cfg.backend == "docker") [ "docker.service" "docker.socket" ] ++ dependsOn; + requires = dependsOn; + environment = proxy_env; + + path = + if cfg.backend == "docker" then [ config.virtualisation.docker.package ] + else if cfg.backend == "podman" then [ config.virtualisation.podman.package ] + else throw "Unhandled backend: ${cfg.backend}"; + + preStart = '' + ${cfg.backend} rm -f ${name} || true + ${optionalString (container.imageFile != null) '' + ${cfg.backend} load -i ${container.imageFile} + ''} + ''; + + # Podman likes knowing what systemd unit launched its container, + # so that it can auto-update containers and restart them. + # + # Sadly, the NixOS `script` option doesn't expose the systemd + # syntax `%n` that would expose the unit location, so we pass it + # as $1 in a scriptArg. + scriptArgs = lib.optionals (cfg.backend == "podman") "%n"; + + script = concatStringsSep " \\\n " ([ + "exec ${cfg.backend} run" + "--rm" + "--name=${escapeShellArg name}" + "--log-driver=${container.log-driver}" + ] ++ optional (container.entrypoint != null) + "--entrypoint=${escapeShellArg container.entrypoint}" + ++ (mapAttrsToList (k: v: "-e ${escapeShellArg k}=${escapeShellArg v}") container.environment) + ++ optional (cfg.backend == "podman") ''-e 'PODMAN_SYSTEMD_UNIT'="$1"'' + ++ map (f: "--env-file ${escapeShellArg f}") container.environmentFiles + ++ map (p: "-p ${escapeShellArg p}") container.ports + ++ optional (container.user != null) "-u ${escapeShellArg container.user}" + ++ map (v: "-v ${escapeShellArg v}") container.volumes + ++ optional (container.workdir != null) "-w ${escapeShellArg container.workdir}" + ++ map escapeShellArg container.extraOptions + ++ [container.image] + ++ map escapeShellArg container.cmd + ); + + preStop = "[ $SERVICE_RESULT = success ] || ${cfg.backend} stop ${name}"; + postStop = "${cfg.backend} rm -f ${name} || true"; + + serviceConfig = { + StandardOutput = "null"; + StandardError = "null"; + + ### There is no generalized way of supporting `reload` for docker + ### containers. Some containers may respond well to SIGHUP sent to their + ### init process, but it is not guaranteed; some apps have other reload + ### mechanisms, some don't have a reload signal at all, and some docker + ### images just have broken signal handling. The best compromise in this + ### case is probably to leave ExecReload undefined, so `systemctl reload` + ### will at least result in an error instead of potentially undefined + ### behaviour. + ### + ### Advanced users can still override this part of the unit to implement + ### a custom reload handler, since the result of all this is a normal + ### systemd service from the perspective of the NixOS module system. + ### + # ExecReload = ...; + ### + + TimeoutStartSec = 0; + TimeoutStopSec = 120; + Restart = "always"; + }; + }; + +in { + imports = [ + ( + lib.mkChangedOptionModule + [ "docker-containers" ] + [ "virtualisation" "oci-containers" ] + (oldcfg: { + backend = "docker"; + containers = lib.mapAttrs (n: v: builtins.removeAttrs (v // { + extraOptions = v.extraDockerOptions or []; + }) [ "extraDockerOptions" ]) oldcfg.docker-containers; + }) + ) + ]; + disabledModules = [ "virtualisation/oci-containers.nix" ]; + + options.virtualisation.oci-containers = { + + backend = mkOption { + type = types.enum [ "podman" "docker" ]; + default = + # TODO: Once https://github.com/NixOS/nixpkgs/issues/77925 is resolved default to podman + # if versionAtLeast config.system.stateVersion "20.09" then "podman" + # else "docker"; + "docker"; + description = "The underlying Docker implementation to use."; + }; + + containers = mkOption { + default = {}; + type = types.attrsOf (types.submodule containerOptions); + description = "OCI (Docker) containers to run as systemd services."; + }; + + }; + + config = lib.mkIf (cfg.containers != {}) (lib.mkMerge [ + { + systemd.services = mapAttrs' (n: v: nameValuePair "${cfg.backend}-${n}" (mkService n v)) cfg.containers; + } + (lib.mkIf (cfg.backend == "podman") { + virtualisation.podman.enable = true; + }) + (lib.mkIf (cfg.backend == "docker") { + virtualisation.docker.enable = true; + }) + ]); + +}