1 { config, pkgs, lib, ... }:
4 cfg = config.services.hydra;
6 baseDir = "/var/lib/hydra";
8 hydraConf = pkgs.writeScript "hydra.conf" cfg.extraConfig;
11 { HYDRA_DBI = cfg.dbi;
12 HYDRA_CONFIG = "${baseDir}/hydra.conf";
13 HYDRA_DATA = "${baseDir}";
17 { NIX_REMOTE = "daemon";
18 PGPASSFILE = "${baseDir}/pgpass";
19 NIX_REMOTE_SYSTEMS = lib.concatStringsSep ":" cfg.buildMachinesFiles;
20 } // lib.optionalAttrs (cfg.smtpHost != null) {
21 EMAIL_SENDER_TRANSPORT = "SMTP";
22 EMAIL_SENDER_TRANSPORT_host = cfg.smtpHost;
23 } // hydraEnv // cfg.extraEnv;
26 { HYDRA_TRACKER = cfg.tracker;
27 XDG_CACHE_HOME = "${baseDir}/www/.cache";
29 PGPASSFILE = "${baseDir}/pgpass-www"; # grrr
30 } // (lib.optionalAttrs cfg.debugServer { DBIC_TRACE = "1"; });
32 localDB = "dbi:Pg:dbname=hydra;user=hydra;";
34 haveLocalDB = cfg.dbi == localDB;
38 makeWrapperArgs = lib.concatStringsSep " " (lib.mapAttrsToList (key: value: "--set-default \"${key}\" \"${value}\"") hydraEnv);
39 in pkgs.buildEnv rec {
41 nativeBuildInputs = [ pkgs.makeWrapper ];
42 paths = [ cfg.package ];
45 if [ -L "$out/bin" ]; then
50 for path in ${lib.concatStringsSep " " paths}; do
51 if [ -d "$path/bin" ]; then
54 if [ -f "$prg" ]; then
56 if [ -x "$prg" ]; then
57 makeWrapper "$path/bin/$prg" "$out/bin/$prg" ${makeWrapperArgs}
74 enable = lib.mkOption {
75 type = lib.types.bool;
78 Whether to run Hydra services.
85 example = "dbi:Pg:dbname=hydra;host=postgres.example.org;user=foo;";
87 The DBI string for Hydra database connection.
89 NOTE: Attempts to set `application_name` will be overridden by
90 `hydra-TYPE` (where TYPE is e.g. `evaluator`, `queue-runner`,
91 etc.) in all hydra services to more easily distinguish where
92 queries are coming from.
96 package = lib.mkPackageOption pkgs "hydra" { };
98 hydraURL = lib.mkOption {
101 The base URL for the Hydra webserver instance. Used for links in emails.
105 listenHost = lib.mkOption {
106 type = lib.types.str;
108 example = "localhost";
110 The hostname or address to listen on or `*` to listen
115 port = lib.mkOption {
116 type = lib.types.port;
119 TCP port the web server should listen to.
123 minimumDiskFree = lib.mkOption {
124 type = lib.types.int;
127 Threshold of minimum disk space (GiB) to determine if the queue runner should run or not.
131 minimumDiskFreeEvaluator = lib.mkOption {
132 type = lib.types.int;
135 Threshold of minimum disk space (GiB) to determine if the evaluator should run or not.
139 notificationSender = lib.mkOption {
140 type = lib.types.str;
142 Sender email address used for email notifications.
146 smtpHost = lib.mkOption {
147 type = lib.types.nullOr lib.types.str;
149 example = "localhost";
151 Hostname of the SMTP server to use to send email.
155 tracker = lib.mkOption {
156 type = lib.types.str;
159 Piece of HTML that is included on all pages.
163 logo = lib.mkOption {
164 type = lib.types.nullOr lib.types.path;
167 Path to a file containing the logo of your Hydra instance.
171 debugServer = lib.mkOption {
172 type = lib.types.bool;
174 description = "Whether to run the server in debug mode.";
177 maxServers = lib.mkOption {
178 type = lib.types.int;
180 description = "Maximum number of starman workers to spawn.";
183 minSpareServers = lib.mkOption {
184 type = lib.types.int;
186 description = "Minimum number of spare starman workers to keep.";
189 maxSpareServers = lib.mkOption {
190 type = lib.types.int;
192 description = "Maximum number of spare starman workers to keep.";
195 extraConfig = lib.mkOption {
196 type = lib.types.lines;
197 description = "Extra lines for the Hydra configuration.";
200 extraEnv = lib.mkOption {
201 type = lib.types.attrsOf lib.types.str;
203 description = "Extra environment variables for Hydra.";
206 gcRootsDir = lib.mkOption {
207 type = lib.types.path;
208 default = "/nix/var/nix/gcroots/hydra";
209 description = "Directory that holds Hydra garbage collector roots.";
212 buildMachinesFiles = lib.mkOption {
213 type = lib.types.listOf lib.types.path;
214 default = lib.optional (config.nix.buildMachines != []) "/etc/nix/machines";
215 defaultText = lib.literalExpression ''lib.optional (config.nix.buildMachines != []) "/etc/nix/machines"'';
216 example = [ "/etc/nix/machines" "/var/lib/hydra/provisioner/machines" ];
217 description = "List of files containing build machines.";
220 useSubstitutes = lib.mkOption {
221 type = lib.types.bool;
224 Whether to use binary caches for downloading store paths. Note that
225 binary substitutions trigger (a potentially large number of) additional
226 HTTP requests that slow down the queue monitor thread significantly.
227 Also, this Hydra instance will serve those downloaded store paths to
228 its users with its own signature attached as if it had built them
229 itself, so don't enable this feature unless your active binary caches
230 are absolute trustworthy.
238 ###### implementation
240 config = lib.mkIf cfg.enable {
243 assertion = cfg.maxServers != 0 && cfg.maxSpareServers != 0 && cfg.minSpareServers != 0;
244 message = "services.hydra.{minSpareServers,maxSpareServers,minSpareServers} cannot be 0";
247 assertion = cfg.minSpareServers < cfg.maxSpareServers;
248 message = "services.hydra.minSpareServers cannot be bigger than services.hydra.maxSpareServers";
252 users.groups.hydra = {
253 gid = config.ids.gids.hydra;
257 { description = "Hydra";
259 # We don't enable `createHome` here because the creation of the home directory is handled by the hydra-init service below.
261 useDefaultShell = true;
262 uid = config.ids.uids.hydra;
265 users.users.hydra-queue-runner =
266 { description = "Hydra queue runner";
268 useDefaultShell = true;
269 home = "${baseDir}/queue-runner"; # really only to keep SSH happy
270 uid = config.ids.uids.hydra-queue-runner;
273 users.users.hydra-www =
274 { description = "Hydra web server";
276 useDefaultShell = true;
277 uid = config.ids.uids.hydra-www;
280 services.hydra.extraConfig =
282 using_frontend_proxy = 1
283 base_uri = ${cfg.hydraURL}
284 notification_sender = ${cfg.notificationSender}
285 max_servers = ${toString cfg.maxServers}
286 ${lib.optionalString (cfg.logo != null) ''
287 hydra_logo = ${cfg.logo}
289 gc_roots_dir = ${cfg.gcRootsDir}
290 use-substitutes = ${if cfg.useSubstitutes then "1" else "0"}
293 environment.systemPackages = [ hydra-package ];
295 environment.variables = hydraEnv;
297 nix.settings = lib.mkMerge [
300 keep-derivations = true;
301 trusted-users = [ "hydra-queue-runner" ];
304 (lib.mkIf (lib.versionOlder (lib.getVersion config.nix.package.out) "2.4pre")
306 # The default (`true') slows Nix down a lot since the build farm
307 # has so many GC roots.
308 gc-check-reachability = false;
313 systemd.slices.system-hydra = {
314 description = "Hydra CI Server Slice";
315 documentation = [ "file://${cfg.package}/share/doc/hydra/index.html" "https://nixos.org/hydra/manual/" ];
318 systemd.services.hydra-init =
319 { wantedBy = [ "multi-user.target" ];
320 requires = lib.optional haveLocalDB "postgresql.service";
321 after = lib.optional haveLocalDB "postgresql.service";
322 environment = env // {
323 HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-init";
325 path = [ pkgs.util-linux ];
328 chown hydra:hydra ${baseDir}
329 chmod 0750 ${baseDir}
331 ln -sf ${hydraConf} ${baseDir}/hydra.conf
333 mkdir -m 0700 -p ${baseDir}/www
334 chown hydra-www:hydra ${baseDir}/www
336 mkdir -m 0700 -p ${baseDir}/queue-runner
337 mkdir -m 0750 -p ${baseDir}/build-logs
338 mkdir -m 0750 -p ${baseDir}/runcommand-logs
339 chown hydra-queue-runner:hydra \
340 ${baseDir}/queue-runner \
341 ${baseDir}/build-logs \
342 ${baseDir}/runcommand-logs
344 ${lib.optionalString haveLocalDB ''
345 if ! [ -e ${baseDir}/.db-created ]; then
346 runuser -u ${config.services.postgresql.superUser} ${config.services.postgresql.package}/bin/createuser hydra
347 runuser -u ${config.services.postgresql.superUser} ${config.services.postgresql.package}/bin/createdb -- -O hydra hydra
348 touch ${baseDir}/.db-created
350 echo "create extension if not exists pg_trgm" | runuser -u ${config.services.postgresql.superUser} -- ${config.services.postgresql.package}/bin/psql hydra
353 if [ ! -e ${cfg.gcRootsDir} ]; then
355 # Move legacy roots directory.
356 if [ -e /nix/var/nix/gcroots/per-user/hydra/hydra-roots ]; then
357 mv /nix/var/nix/gcroots/per-user/hydra/hydra-roots ${cfg.gcRootsDir}
360 mkdir -p ${cfg.gcRootsDir}
363 # Move legacy hydra-www roots.
364 if [ -e /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots ]; then
365 find /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots/ -type f \
366 | xargs -r mv -f -t ${cfg.gcRootsDir}/
367 rmdir /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots
370 chown hydra:hydra ${cfg.gcRootsDir}
371 chmod 2775 ${cfg.gcRootsDir}
373 serviceConfig.ExecStart = "${hydra-package}/bin/hydra-init";
374 serviceConfig.PermissionsStartOnly = true;
375 serviceConfig.User = "hydra";
376 serviceConfig.Type = "oneshot";
377 serviceConfig.RemainAfterExit = true;
378 serviceConfig.Slice = "system-hydra.slice";
381 systemd.services.hydra-server =
382 { wantedBy = [ "multi-user.target" ];
383 requires = [ "hydra-init.service" ];
384 after = [ "hydra-init.service" ];
385 environment = serverEnv // {
386 HYDRA_DBI = "${serverEnv.HYDRA_DBI};application_name=hydra-server";
388 restartTriggers = [ hydraConf ];
391 "@${hydra-package}/bin/hydra-server hydra-server -f -h '${cfg.listenHost}' "
392 + "-p ${toString cfg.port} --min_spare_servers ${toString cfg.minSpareServers} --max_spare_servers ${toString cfg.maxSpareServers} "
393 + "--max_servers ${toString cfg.maxServers} --max_requests 100 ${lib.optionalString cfg.debugServer "-d"}";
395 PermissionsStartOnly = true;
397 Slice = "system-hydra.slice";
401 systemd.services.hydra-queue-runner =
402 { wantedBy = [ "multi-user.target" ];
403 requires = [ "hydra-init.service" ];
404 after = [ "hydra-init.service" "network.target" ];
405 path = [ hydra-package pkgs.nettools pkgs.openssh pkgs.bzip2 config.nix.package ];
406 restartTriggers = [ hydraConf ];
407 environment = env // {
408 PGPASSFILE = "${baseDir}/pgpass-queue-runner"; # grrr
409 IN_SYSTEMD = "1"; # to get log severity levels
410 HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-queue-runner";
413 { ExecStart = "@${hydra-package}/bin/hydra-queue-runner hydra-queue-runner -v";
414 ExecStopPost = "${hydra-package}/bin/hydra-queue-runner --unlock";
415 User = "hydra-queue-runner";
417 Slice = "system-hydra.slice";
419 # Ensure we can get core dumps.
420 LimitCORE = "infinity";
421 WorkingDirectory = "${baseDir}/queue-runner";
425 systemd.services.hydra-evaluator =
426 { wantedBy = [ "multi-user.target" ];
427 requires = [ "hydra-init.service" ];
428 wants = [ "network-online.target" ];
429 after = [ "hydra-init.service" "network.target" "network-online.target" ];
430 path = with pkgs; [ hydra-package nettools jq ];
431 restartTriggers = [ hydraConf ];
432 environment = env // {
433 HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-evaluator";
436 { ExecStart = "@${hydra-package}/bin/hydra-evaluator hydra-evaluator";
439 WorkingDirectory = baseDir;
440 Slice = "system-hydra.slice";
444 systemd.services.hydra-update-gc-roots =
445 { requires = [ "hydra-init.service" ];
446 after = [ "hydra-init.service" ];
447 environment = env // {
448 HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-update-gc-roots";
451 { ExecStart = "@${hydra-package}/bin/hydra-update-gc-roots hydra-update-gc-roots";
453 Slice = "system-hydra.slice";
458 systemd.services.hydra-send-stats =
459 { wantedBy = [ "multi-user.target" ];
460 after = [ "hydra-init.service" ];
461 environment = env // {
462 HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-send-stats";
465 { ExecStart = "@${hydra-package}/bin/hydra-send-stats hydra-send-stats";
467 Slice = "system-hydra.slice";
471 systemd.services.hydra-notify =
472 { wantedBy = [ "multi-user.target" ];
473 requires = [ "hydra-init.service" ];
474 after = [ "hydra-init.service" ];
475 restartTriggers = [ hydraConf ];
476 path = [ pkgs.zstd ];
477 environment = env // {
478 PGPASSFILE = "${baseDir}/pgpass-queue-runner";
479 HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-notify";
482 { ExecStart = "@${hydra-package}/bin/hydra-notify hydra-notify";
483 # FIXME: run this under a less privileged user?
484 User = "hydra-queue-runner";
487 Slice = "system-hydra.slice";
491 # If there is less than a certain amount of free disk space, stop
492 # the queue/evaluator to prevent builds from failing or aborting.
493 systemd.services.hydra-check-space =
496 if [ $(($(stat -f -c '%a' /nix/store) * $(stat -f -c '%S' /nix/store))) -lt $((${toString cfg.minimumDiskFree} * 1024**3)) ]; then
497 echo "stopping Hydra queue runner due to lack of free space..."
498 systemctl stop hydra-queue-runner
500 if [ $(($(stat -f -c '%a' /nix/store) * $(stat -f -c '%S' /nix/store))) -lt $((${toString cfg.minimumDiskFreeEvaluator} * 1024**3)) ]; then
501 echo "stopping Hydra evaluator due to lack of free space..."
502 systemctl stop hydra-evaluator
506 serviceConfig.Slice = "system-hydra.slice";
509 # Periodically compress build logs. The queue runner compresses
510 # logs automatically after a step finishes, but this doesn't work
511 # if the queue runner is stopped prematurely.
512 systemd.services.hydra-compress-logs =
513 { path = [ pkgs.bzip2 pkgs.zstd ];
517 compression=$(sed -nr 's/compress_build_logs_compression = ()/\1/p' ${baseDir}/hydra.conf)
518 if [[ $compression == "" ]]; then
520 elif [[ $compression == zstd ]]; then
521 compression="zstd --rm"
523 find ${baseDir}/build-logs -type f -name "*.drv" -mtime +3 -size +0c | xargs -r "$compression" --force --quiet
525 startAt = "Sun 01:45";
526 serviceConfig.Slice = "system-hydra.slice";
529 services.postgresql.enable = lib.mkIf haveLocalDB true;
531 services.postgresql.identMap = lib.optionalString haveLocalDB
533 hydra-users hydra hydra
534 hydra-users hydra-queue-runner hydra
535 hydra-users hydra-www hydra
536 hydra-users root hydra
537 # The postgres user is used to create the pg_trgm extension for the hydra database
538 hydra-users postgres postgres
541 services.postgresql.authentication = lib.optionalString haveLocalDB
543 local hydra all ident map=hydra-users