1 { config, lib, pkgs, ... }:
3 cfg = config.services.zammad;
4 settingsFormat = pkgs.formats.yaml { };
5 filterNull = lib.filterAttrs (_: v: v != null);
13 StateDirectory = "zammad";
14 WorkingDirectory = cfg.dataDir;
17 RAILS_ENV = "production";
18 NODE_ENV = "production";
19 RAILS_SERVE_STATIC_FILES = "true";
20 RAILS_LOG_TO_STDOUT = "true";
21 REDIS_URL = "redis://${cfg.redis.host}:${toString cfg.redis.port}";
23 databaseConfig = settingsFormat.generate "database.yml" cfg.database.settings;
29 enable = lib.mkEnableOption "Zammad, a web-based, open source user support/ticketing solution";
31 package = lib.mkPackageOption pkgs "zammad" { };
33 dataDir = lib.mkOption {
34 type = lib.types.path;
35 default = "/var/lib/zammad";
37 Path to a folder that will contain Zammad working directory.
43 default = "127.0.0.1";
44 example = "192.168.23.42";
45 description = "Host address.";
48 openPorts = lib.mkOption {
49 type = lib.types.bool;
51 description = "Whether to open firewall ports for Zammad";
55 type = lib.types.port;
57 description = "Web service port.";
60 websocketPort = lib.mkOption {
61 type = lib.types.port;
63 description = "Websocket service port.";
67 createLocally = lib.mkOption {
68 type = lib.types.bool;
70 description = "Whether to create a local redis automatically.";
77 Name of the redis server. Only used if `createLocally` is set to true.
83 default = "localhost";
90 type = lib.types.port;
92 description = "Port of the redis server.";
98 type = lib.types.enum [ "PostgreSQL" "MySQL" ];
99 default = "PostgreSQL";
101 description = "Database engine to use.";
104 host = lib.mkOption {
105 type = lib.types.nullOr lib.types.str;
107 PostgreSQL = "/run/postgresql";
109 }.${cfg.database.type};
110 defaultText = lib.literalExpression ''
112 PostgreSQL = "/run/postgresql";
114 }.''${config.services.zammad.database.type};
117 Database host address.
121 port = lib.mkOption {
122 type = lib.types.nullOr lib.types.port;
124 description = "Database port. Use `null` for default port.";
127 name = lib.mkOption {
128 type = lib.types.str;
135 user = lib.mkOption {
136 type = lib.types.nullOr lib.types.str;
138 description = "Database user.";
141 passwordFile = lib.mkOption {
142 type = lib.types.nullOr lib.types.path;
144 example = "/run/keys/zammad-dbpassword";
146 A file containing the password for {option}`services.zammad.database.user`.
150 createLocally = lib.mkOption {
151 type = lib.types.bool;
153 description = "Whether to create a local database automatically.";
156 settings = lib.mkOption {
157 type = settingsFormat.type;
159 example = lib.literalExpression ''
164 The {file}`database.yml` configuration file as key value set.
166 for list of configuration parameters.
171 secretKeyBaseFile = lib.mkOption {
172 type = lib.types.nullOr lib.types.path;
174 example = "/run/keys/secret_key_base";
176 The path to a file containing the
177 `secret_key_base` secret.
179 Zammad uses `secret_key_base` to encrypt
180 the cookie store, which contains session data, and to digest
183 Needs to be a 64 byte long string of hexadecimal
184 characters. You can generate one by running
187 openssl rand -hex 64 >/path/to/secret_key_base_file
190 This should be a string, not a nix path, since nix paths are
191 copied into the world-readable nix store.
197 config = lib.mkIf cfg.enable {
199 services.zammad.database.settings = {
200 production = lib.mapAttrs (_: v: lib.mkDefault v) (filterNull {
202 PostgreSQL = "postgresql";
204 }.${cfg.database.type};
205 database = cfg.database.name;
209 username = cfg.database.user;
210 host = cfg.database.host;
211 port = cfg.database.port;
215 networking.firewall.allowedTCPPorts = lib.mkIf cfg.openPorts [
216 config.services.zammad.port
217 config.services.zammad.websocketPort
220 users.users.zammad = {
226 users.groups.zammad = { };
230 assertion = cfg.database.createLocally -> cfg.database.user == "zammad" && cfg.database.name == "zammad";
231 message = "services.zammad.database.user must be set to \"zammad\" if services.zammad.database.createLocally is set to true";
234 assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
235 message = "a password cannot be specified if services.zammad.database.createLocally is set to true";
238 assertion = cfg.redis.createLocally -> cfg.redis.host == "localhost";
239 message = "the redis host must be localhost if services.zammad.redis.createLocally is set to true";
243 services.mysql = lib.optionalAttrs (cfg.database.createLocally && cfg.database.type == "MySQL") {
245 package = lib.mkDefault pkgs.mariadb;
246 ensureDatabases = [ cfg.database.name ];
249 name = cfg.database.user;
250 ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
255 services.postgresql = lib.optionalAttrs (cfg.database.createLocally && cfg.database.type == "PostgreSQL") {
257 ensureDatabases = [ cfg.database.name ];
260 name = cfg.database.user;
261 ensureDBOwnership = true;
266 services.redis = lib.optionalAttrs cfg.redis.createLocally {
267 servers."${cfg.redis.name}" = {
269 port = cfg.redis.port;
273 systemd.services.zammad-web = {
275 serviceConfig = serviceConfig // {
276 # loading all the gems takes time
277 TimeoutStartSec = 1200;
282 ] ++ lib.optionals cfg.redis.createLocally [
283 "redis-${cfg.redis.name}.service"
288 description = "Zammad web";
289 wantedBy = [ "multi-user.target" ];
291 # Blindly copy the whole project here.
293 rm -rf ./public/assets/
296 cp -r --no-preserve=owner ${cfg.package}/* .
299 cp ${databaseConfig} ./config/database.yml
301 ${lib.optionalString (cfg.database.passwordFile != null) ''
303 echo -n " password: "
304 cat ${cfg.database.passwordFile}
305 } >> ./config/database.yml
307 ${lib.optionalString (cfg.secretKeyBaseFile != null) ''
310 echo -n " secret_key_base: "
311 cat ${cfg.secretKeyBaseFile}
312 } > ./config/secrets.yml
315 if [ `${config.services.postgresql.package}/bin/psql \
316 --host ${cfg.database.host} \
318 (cfg.database.port != null)
319 "--port ${toString cfg.database.port}"} \
320 --username ${cfg.database.user} \
321 --dbname ${cfg.database.name} \
322 --command "SELECT COUNT(*) FROM pg_class c \
323 JOIN pg_namespace s ON s.oid = c.relnamespace \
324 WHERE s.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema') \
325 AND s.nspname NOT LIKE 'pg_temp%';" | sed -n 3p` -eq 0 ]; then
326 echo "Initialize database"
327 ./bin/rake --no-system db:migrate
328 ./bin/rake --no-system db:seed
330 echo "Migrate database"
331 ./bin/rake --no-system db:migrate
335 script = "./script/rails server -b ${cfg.host} -p ${toString cfg.port}";
338 systemd.services.zammad-websocket = {
339 inherit serviceConfig environment;
340 after = [ "zammad-web.service" ];
341 requires = [ "zammad-web.service" ];
342 description = "Zammad websocket";
343 wantedBy = [ "multi-user.target" ];
344 script = "./script/websocket-server.rb -b ${cfg.host} -p ${toString cfg.websocketPort} start";
347 systemd.services.zammad-worker = {
348 inherit serviceConfig environment;
349 after = [ "zammad-web.service" ];
350 requires = [ "zammad-web.service" ];
351 description = "Zammad background worker";
352 wantedBy = [ "multi-user.target" ];
353 script = "./script/background-worker.rb start";
357 meta.maintainers = with lib.maintainers; [ taeer netali ];