{ config, lib, pkgs, ... }: let cfg = config.services.overleaf-oci; domain = config.networking.domain; subdomain = cfg.reverseProxy.subdomain; fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain; mongodbInitReplicaSet = ./mongodb-init-replica-set.js; images = { # https://hub.docker.com/r/sharelatex/sharelatex sharelatex = pkgs.dockerTools.pullImage { imageName = "sharelatex/sharelatex"; imageDigest = "sha256:fa5e5a049721b0973aa6ddabd15cb04da2d492f38c685fda0805dab97d4f3c8a"; hash = "sha256-2Om6b+xPcBFObMKAlZ+qsvPw1xd8B/8ODyI5dsWoaQA="; finalImageName = "sharelatex/sharelatex"; finalImageTag = "6.1.2"; }; # https://hub.docker.com/_/mongo mongo = pkgs.dockerTools.pullImage { imageName = "mongo"; imageDigest = "sha256:ce32a4b67580c982e2020b07d4ffafdbd9ce474088434294d89c6bd9257f7788"; hash = "sha256-NtMSeH5hYD69uF7gZWPppxoI9esyPTfxaw50yh0ChpY="; finalImageName = "mongo"; finalImageTag = "8.3.1"; }; # https://hub.docker.com/_/redis redis = pkgs.dockerTools.pullImage { imageName = "redis"; imageDigest = "sha256:0c341492924cad6f5483f9133e43bd6c51ecdecbcadfac5b51657393b6a7936c"; hash = "sha256-nU5+UdvA+29sbr7dAD2jpKYACxYb3BfStlOE/25ET+w="; finalImageName = "redis"; finalImageTag = "8.6.3"; }; }; defaultEnv = { EMAIL_CONFIRMATION_DISABLED = "true"; ENABLED_LINKED_FILE_TYPES = "project_file,project_output_file"; ENABLE_CONVERSIONS = "true"; OVERLEAF_APP_NAME = "Overleaf Community Edition"; OVERLEAF_MONGO_URL = "mongodb://mongo/sharelatex"; OVERLEAF_REDIS_HOST = "redis"; REDIS_HOST = "redis"; }; inherit (lib) mkEnableOption mkIf mkOption mkOverride optional types ; inherit (lib.utils) mkReverseProxyOption mkVirtualHost ; in { options.services.overleaf-oci = { enable = mkEnableOption "Overleaf service stack with Podman"; port = mkOption { type = types.port; default = 80; description = "The port Overleaf will listen on."; }; environment = mkOption { type = types.attrsOf types.str; default = { }; description = "Extra environment variables for Overleaf."; }; environmentFile = mkOption { type = types.nullOr types.path; default = null; description = "Environment file for secrets."; }; reverseProxy = mkReverseProxyOption "Overleaf" "overleaf"; }; config = mkIf cfg.enable { virtualisation.podman = { enable = true; autoPrune.enable = true; dockerCompat = true; }; services.nginx.virtualHosts = mkIf cfg.reverseProxy.enable { "${fqdn}" = mkVirtualHost { inherit (cfg) port; ssl = cfg.reverseProxy.forceSSL; }; }; networking.firewall.interfaces = let matchAll = if !config.networking.nftables.enable then "podman+" else "podman*"; in { "${matchAll}".allowedUDPPorts = [ 53 ]; }; virtualisation.oci-containers.backend = "podman"; virtualisation.oci-containers.containers = { overleaf-mongo = { image = with images.mongo; imageName + ":" + imageTag; imageFile = images.mongo; environment = { MONGO_INITDB_DATABASE = "sharelatex"; }; volumes = [ "overleaf_mongo-data:/data/db:rw" "${mongodbInitReplicaSet}:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js:rw" ]; cmd = [ "--replSet" "overleaf" ]; log-driver = "journald"; extraOptions = [ "--network-alias=mongo" "--network=overleaf_default" "--add-host=mongo:127.0.0.1" "--health-cmd=echo 'db.stats().ok' | mongosh localhost:27017/test --quiet" "--health-interval=10s" "--health-retries=5" "--health-timeout=10s" ]; }; overleaf-redis = { image = with images.redis; imageName + ":" + imageTag; imageFile = images.redis; volumes = [ "overleaf_redis-data:/data:rw" ]; log-driver = "journald"; extraOptions = [ "--network-alias=redis" "--network=overleaf_default" ]; }; overleaf-sharelatex = { image = with images.sharelatex; imageName + ":" + imageTag; imageFile = images.sharelatex; log-driver = "journald"; environment = defaultEnv // cfg.environment; environmentFiles = optional (cfg.environmentFile != null) cfg.environmentFile; volumes = [ "overleaf_data:/var/lib/overleaf:rw" ]; ports = [ "${toString cfg.port}:80/tcp" ]; extraOptions = [ "--network-alias=sharelatex" "--network=overleaf_default" ]; }; }; systemd.services = let commonServiceCfg = { serviceConfig.Restart = mkOverride 90 "always"; partOf = [ "podman-compose-overleaf-root.target" ]; wantedBy = [ "podman-compose-overleaf-root.target" ]; }; in { podman-overleaf-mongo = commonServiceCfg // { after = [ "podman-network-overleaf_default.service" "podman-volume-overleaf_mongo-data.service" ]; requires = [ "podman-network-overleaf_default.service" "podman-volume-overleaf_mongo-data.service" ]; }; podman-overleaf-redis = commonServiceCfg // { after = [ "podman-network-overleaf_default.service" "podman-volume-overleaf_redis-data.service" ]; requires = [ "podman-network-overleaf_default.service" "podman-volume-overleaf_redis-data.service" ]; }; podman-overleaf-sharelatex = commonServiceCfg // { after = [ "podman-network-overleaf_default.service" "podman-overleaf-mongo.service" "podman-overleaf-redis.service" ]; requires = [ "podman-network-overleaf_default.service" "podman-overleaf-mongo.service" "podman-overleaf-redis.service" ]; serviceConfig.ExecStartPre = [ "${pkgs.bash}/bin/bash -c 'until ${pkgs.podman}/bin/podman healthcheck run overleaf-mongo; do sleep 2; done'" ]; }; podman-network-overleaf_default = { path = [ pkgs.podman ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; ExecStop = "podman network rm -f overleaf_default"; }; script = '' podman network inspect overleaf_default || podman network create overleaf_default ''; partOf = [ "podman-compose-overleaf-root.target" ]; wantedBy = [ "podman-compose-overleaf-root.target" ]; }; podman-volume-overleaf_redis-data = { path = [ pkgs.podman ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; script = '' podman volume inspect overleaf_redis-data || podman volume create overleaf_redis-data ''; partOf = [ "podman-compose-overleaf-root.target" ]; wantedBy = [ "podman-compose-overleaf-root.target" ]; }; podman-volume-overleaf_mongo-data = { path = [ pkgs.podman ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; script = '' podman volume inspect overleaf_mongo-data || podman volume create overleaf_mongo-data ''; partOf = [ "podman-compose-overleaf-root.target" ]; wantedBy = [ "podman-compose-overleaf-root.target" ]; }; }; systemd.targets."podman-compose-overleaf-root" = { unitConfig.Description = "Root target for Overleaf services."; wantedBy = [ "multi-user.target" ]; }; }; }