{ pkgs, config, lib, ... }: with lib; let cfg = config.cloud.grist; mkImage = { imageName, imageDigest, ... }: "${imageName}@${imageDigest}"; # If we can pullImage we can just do # mkImage = pkgs.dockerTools.pullImage; images = { # https://hub.docker.com/r/gristlabs/grist/tags grist = mkImage { imageName = "docker.io/gristlabs/grist-oss"; finalImageTag = "1.4.2"; imageDigest = "sha256:508ed0024f08702ae8797a6607e42ca67e1a0be0ac95c02e75c2a226b5e9cb9b"; }; # https://hub.docker.com/r/valkey/valkey/tags valkey = mkImage { imageName = "docker.io/valkey/valkey"; finalImageTag = "8.0.2-alpine"; imageDigest = "sha256:0fae58181c223280867e8b6d9d5fa29fca507770aeb6819f36d059cab73fa2fd"; }; }; defaultEnv = { GRIST_HIDE_UI_ELEMENTS = lib.concatStringsSep "," [ "helpCenter" "billing" "multiAccounts" "supportGrist" ]; GRIST_PAGE_TITLE_SUFFIX = " - DTTH Grist"; GRIST_FORCE_LOGIN = "true"; GRIST_WIDGET_LIST_URL = "https://github.com/gristlabs/grist-widget/releases/download/latest/manifest.json"; GRIST_EXTERNAL_ATTACHMENTS_MODE = "snapshots"; GRIST_SANDBOX_FLAVOR = "gvisor"; PYTHON_VERSION = "3"; PYTHON_VERSION_ON_CREATION = "3"; }; in { options.cloud.grist = { enable = mkEnableOption "Grist database server"; envFile = mkOption { type = types.path; description = "Path to an environment file that specifies GRIST_SESSION_SECRET and others"; }; host = mkOption { type = types.str; description = "Exposed hostname"; }; port = mkOption { type = types.int; description = "Exposed port"; default = 9674; }; dataDir = mkOption { type = types.str; description = "Path to the data directory"; }; settings = { allowedWebhookDomains = mkOption { type = types.listOf types.str; description = "List of domains to be allowed in webhooks"; default = [ "dtth.ch" "nkagami.me" "discord.com" ]; }; defaultEmail = mkOption { type = types.str; description = "Default email address for admin user"; default = "nki@nkagami.me"; }; }; }; config = mkIf cfg.enable { cloud.traefik.hosts.grist = { inherit (cfg) port host; }; systemd.services.arion-grist = { serviceConfig.Type = "notify"; serviceConfig.NotifyAccess = "all"; serviceConfig.TimeoutSec = 300; script = lib.mkBefore '' ${lib.getExe pkgs.wait4x} http http://127.0.0.1:${toString cfg.port} -t 0 -q -- systemd-notify --ready & ''; unitConfig.RequiresMountsFor = [ cfg.dataDir ]; unitConfig.ReadWritePaths = [ cfg.dataDir ]; }; virtualisation.arion.projects.grist.settings = { services.grist-server.service = { image = images.grist; restart = "unless-stopped"; volumes = [ "${cfg.dataDir}:/persist" ]; environment = defaultEnv // { APP_HOME_URL = "https://${cfg.host}"; ALLOWED_WEBHOOK_DOMAINS = lib.concatStringsSep "," cfg.settings.allowedWebhookDomains; GRIST_DEFAULT_EMAIL = cfg.settings.defaultEmail; REDIS_URL = "redis://valkey/1"; }; env_file = [ cfg.envFile ]; ports = [ "127.0.0.1:${toString cfg.port}:8484" ]; }; services.valkey.service = { image = images.valkey; command = "--save 60 1 --loglevel warning"; restart = "unless-stopped"; healthcheck = { test = [ "CMD-SHELL" "valkey-cli ping | grep PONG" ]; start_period = "20s"; interval = "30s"; retries = 5; timeout = "3s"; }; volumes = [ "valkey:/data" ]; }; docker-compose.volumes = { valkey.driver = "local"; }; }; systemd.tmpfiles.settings."10-grist".${cfg.dataDir}.d = { user = "root"; group = "root"; mode = "0700"; }; }; }