1{
2 config,
3 pkgs,
4 lib,
5 utils,
6 ...
7}:
8let
9 cfg = config.services.pghero;
10 settingsFormat = pkgs.formats.yaml { };
11 settingsFile = settingsFormat.generate "pghero.yaml" cfg.settings;
12in
13{
14 options.services.pghero = {
15 enable = lib.mkEnableOption "PgHero service";
16 package = lib.mkPackageOption pkgs "pghero" { };
17
18 listenAddress = lib.mkOption {
19 type = lib.types.str;
20 example = "[::1]:3000";
21 description = ''
22 `hostname:port` to listen for HTTP traffic.
23
24 This is bound using the systemd socket activation.
25 '';
26 };
27
28 extraArgs = lib.mkOption {
29 type = lib.types.listOf lib.types.str;
30 default = [ ];
31 description = ''
32 Additional command-line arguments for the systemd service.
33
34 Refer to the [Puma web server documentation] for available arguments.
35
36 [Puma web server documentation]: https://puma.io/puma#configuration
37 '';
38 };
39
40 settings = lib.mkOption {
41 type = settingsFormat.type;
42 default = { };
43 example = {
44 databases = {
45 primary = {
46 url = "<%= ENV['PRIMARY_DATABASE_URL'] %>";
47 };
48 };
49 };
50 description = ''
51 PgHero configuration. Refer to the [PgHero documentation] for more
52 details.
53
54 [PgHero documentation]: https://github.com/ankane/pghero/blob/master/guides/Linux.md#multiple-databases
55 '';
56 };
57
58 environment = lib.mkOption {
59 type = lib.types.attrsOf lib.types.str;
60 default = { };
61 description = ''
62 Environment variables to set for the service. Secrets should be
63 specified using {option}`environmentFile`.
64 '';
65 };
66
67 environmentFiles = lib.mkOption {
68 type = lib.types.listOf lib.types.path;
69 default = [ ];
70 description = ''
71 File to load environment variables from. Loaded variables override
72 values set in {option}`environment`.
73 '';
74 };
75
76 extraGroups = lib.mkOption {
77 type = lib.types.listOf lib.types.str;
78 default = [ ];
79 example = [ "tlskeys" ];
80 description = ''
81 Additional groups for the systemd service.
82 '';
83 };
84 };
85
86 config = lib.mkIf cfg.enable {
87 systemd.sockets.pghero = {
88 unitConfig.Description = "PgHero HTTP socket";
89 wantedBy = [ "sockets.target" ];
90 listenStreams = [ cfg.listenAddress ];
91 };
92
93 systemd.services.pghero = {
94 description = "PgHero performance dashboard for PostgreSQL";
95 wantedBy = [ "multi-user.target" ];
96 requires = [ "pghero.socket" ];
97 after = [
98 "pghero.socket"
99 "network.target"
100 ];
101
102 environment = {
103 RAILS_ENV = "production";
104 PGHERO_CONFIG_PATH = settingsFile;
105 } // cfg.environment;
106
107 serviceConfig = {
108 Type = "notify";
109 WatchdogSec = "10";
110
111 ExecStart = utils.escapeSystemdExecArgs (
112 [
113 (lib.getExe cfg.package)
114 "--bind-to-activated-sockets"
115 "only"
116 ]
117 ++ cfg.extraArgs
118 );
119 Restart = "always";
120
121 WorkingDirectory = "${cfg.package}/share/pghero";
122
123 EnvironmentFile = cfg.environmentFiles;
124 SupplementaryGroups = cfg.extraGroups;
125
126 DynamicUser = true;
127 UMask = "0077";
128
129 ProtectHome = true;
130 ProtectProc = "invisible";
131 ProcSubset = "pid";
132 ProtectClock = true;
133 ProtectHostname = true;
134 ProtectControlGroups = true;
135 ProtectKernelLogs = true;
136 ProtectKernelModules = true;
137 ProtectKernelTunables = true;
138 PrivateUsers = true;
139 PrivateDevices = true;
140 RestrictRealtime = true;
141 RestrictNamespaces = true;
142 RestrictAddressFamilies = [
143 "AF_INET"
144 "AF_INET6"
145 "AF_UNIX"
146 ];
147 DeviceAllow = [ "" ];
148 DevicePolicy = "closed";
149 CapabilityBoundingSet = [ "" ];
150 MemoryDenyWriteExecute = true;
151 LockPersonality = true;
152 SystemCallArchitectures = "native";
153 SystemCallErrorNumber = "EPERM";
154 SystemCallFilter = [ "@system-service" ];
155 };
156 };
157 };
158}