]> git.immae.eu Git - perso/Immae/Config/Nix.git/blob - flakes/mastodon/flake.nix
1d0db10d7af988c9500396ad5a22c70bbff9440b
[perso/Immae/Config/Nix.git] / flakes / mastodon / flake.nix
1 {
2 description = "Your self-hosted, globally interconnected microblogging community";
3 inputs.myuids = {
4 url = "path:../myuids";
5 };
6 inputs.flake-utils.url = "github:numtide/flake-utils";
7 inputs.nixpkgs = {
8 url = "github:NixOS/nixpkgs/840c782d507d60aaa49aa9e3f6d0b0e780912742";
9 flake = false;
10 };
11 inputs.mastodon = {
12 url = "github:tootsuite/mastodon/v2.9.4";
13 flake = false;
14 };
15
16 outputs = { self, myuids, nixpkgs, mastodon, flake-utils }: flake-utils.lib.eachSystem ["x86_64-linux"] (system:
17 let
18 pkgs = import nixpkgs { inherit system; overlays = []; };
19 version = (builtins.fromJSON (builtins.readFile ./flake.lock)).nodes.mastodon.original.ref;
20 inherit (pkgs) callPackage;
21 in rec {
22 packages.mastodon = callPackage ./. { src = mastodon // { inherit version; }; };
23 defaultPackage = packages.mastodon;
24 legacyPackages.mastodon = packages.mastodon;
25 checks = {
26 build = defaultPackage;
27 };
28 }
29 ) // rec {
30 overlays = {
31 mastodon = final: prev: {
32 mastodon = self.defaultPackage."${final.system}";
33 };
34 };
35 overlay = overlays.mastodon;
36 nixosModule = { lib, pkgs, config, ... }:
37 let
38 name = "mastodon";
39 cfg = config.immaeServices.mastodon;
40 in
41 {
42 options.immaeServices.mastodon = {
43 enable = lib.mkEnableOption "Enable Mastodon’s service";
44 user = lib.mkOption {
45 type = lib.types.str;
46 default = name;
47 description = "User account under which Mastodon runs";
48 };
49 group = lib.mkOption {
50 type = lib.types.str;
51 default = name;
52 description = "Group under which Mastodon runs";
53 };
54 dataDir = lib.mkOption {
55 type = lib.types.path;
56 default = "/var/lib/${name}";
57 description = ''
58 The directory where Mastodon stores its data.
59 '';
60 };
61 socketsPrefix = lib.mkOption {
62 type = lib.types.str;
63 default = "live";
64 description = ''
65 The prefix to use for Mastodon sockets.
66 '';
67 };
68 socketsDir = lib.mkOption {
69 type = lib.types.path;
70 default = "/run/${name}";
71 description = ''
72 The directory where Mastodon puts runtime files and sockets.
73 '';
74 };
75 configFile = lib.mkOption {
76 type = lib.types.path;
77 description = ''
78 The configuration file path for Mastodon.
79 '';
80 };
81 package = lib.mkOption {
82 type = lib.types.package;
83 default = pkgs.mastodon;
84 description = ''
85 Mastodon package to use.
86 '';
87 };
88 # Output variables
89 workdir = lib.mkOption {
90 type = lib.types.package;
91 default = cfg.package.override { varDir = cfg.dataDir; };
92 description = ''
93 Adjusted mastodon package with overriden varDir
94 '';
95 readOnly = true;
96 };
97 systemdStateDirectory = lib.mkOption {
98 type = lib.types.str;
99 # Use ReadWritePaths= instead if varDir is outside of /var/lib
100 default = assert lib.strings.hasPrefix "/var/lib/" cfg.dataDir;
101 lib.strings.removePrefix "/var/lib/" cfg.dataDir;
102 description = ''
103 Adjusted Mastodon data directory for systemd
104 '';
105 readOnly = true;
106 };
107 systemdRuntimeDirectory = lib.mkOption {
108 type = lib.types.str;
109 # Use ReadWritePaths= instead if socketsDir is outside of /run
110 default = assert lib.strings.hasPrefix "/run/" cfg.socketsDir;
111 lib.strings.removePrefix "/run/" cfg.socketsDir;
112 description = ''
113 Adjusted Mastodon sockets directory for systemd
114 '';
115 readOnly = true;
116 };
117 sockets = lib.mkOption {
118 type = lib.types.attrsOf lib.types.path;
119 default = {
120 node = "${cfg.socketsDir}/${cfg.socketsPrefix}_node.sock";
121 rails = "${cfg.socketsDir}/${cfg.socketsPrefix}_puma.sock";
122 };
123 readOnly = true;
124 description = ''
125 Mastodon sockets
126 '';
127 };
128 };
129
130 config = lib.mkIf cfg.enable {
131 nixpkgs.overlays = [ self.overlay ];
132 users.users = lib.optionalAttrs (cfg.user == name) {
133 "${name}" = {
134 uid = myuids.lib.uids.mastodon;
135 group = cfg.group;
136 description = "Mastodon user";
137 home = cfg.dataDir;
138 useDefaultShell = true;
139 };
140 };
141 users.groups = lib.optionalAttrs (cfg.group == name) {
142 "${name}" = {
143 gid = myuids.lib.gids.mastodon;
144 };
145 };
146
147 systemd.slices.mastodon = {
148 description = "Mastodon slice";
149 };
150
151 systemd.services.mastodon-streaming = {
152 description = "Mastodon Streaming";
153 wantedBy = [ "multi-user.target" ];
154 after = [ "network.target" "mastodon-web.service" ];
155
156 environment.NODE_ENV = "production";
157 environment.SOCKET = cfg.sockets.node;
158
159 path = [ cfg.workdir.nodejs pkgs.bashInteractive ];
160
161 script = ''
162 exec npm run start
163 '';
164
165 postStart = ''
166 while [ ! -S $SOCKET ]; do
167 sleep 0.5
168 done
169 chmod a+w $SOCKET
170 '';
171
172 postStop = ''
173 rm $SOCKET
174 '';
175
176 serviceConfig = {
177 Slice = "mastodon.slice";
178 User = cfg.user;
179 EnvironmentFile = cfg.configFile;
180 PrivateTmp = true;
181 Restart = "always";
182 TimeoutSec = 15;
183 Type = "simple";
184 WorkingDirectory = cfg.workdir;
185 StateDirectory = cfg.systemdStateDirectory;
186 RuntimeDirectory = cfg.systemdRuntimeDirectory;
187 RuntimeDirectoryPreserve = "yes";
188 };
189
190 unitConfig.RequiresMountsFor = cfg.dataDir;
191 };
192
193 systemd.services.mastodon-web = {
194 description = "Mastodon Web app";
195 wantedBy = [ "multi-user.target" ];
196 after = [ "network.target" ];
197
198 environment.RAILS_ENV = "production";
199 environment.BUNDLE_PATH = "${cfg.workdir.gems}/${cfg.workdir.gems.ruby.gemPath}";
200 environment.BUNDLE_GEMFILE = "${cfg.workdir.gems.confFiles}/Gemfile";
201 environment.SOCKET = cfg.sockets.rails;
202
203 path = [ cfg.workdir.gems cfg.workdir.gems.ruby pkgs.file pkgs.imagemagick ];
204
205 preStart = ''
206 install -m 0755 -d ${cfg.dataDir}/tmp/cache
207 ./bin/bundle exec rails db:migrate
208 '';
209
210 script = ''
211 exec ./bin/bundle exec puma -C config/puma.rb
212 '';
213
214 postStart = ''
215 exec ./bin/tootctl cache clear
216 '';
217 serviceConfig = {
218 Slice = "mastodon.slice";
219 User = cfg.user;
220 EnvironmentFile = cfg.configFile;
221 PrivateTmp = true;
222 Restart = "always";
223 TimeoutSec = 60;
224 Type = "simple";
225 WorkingDirectory = cfg.workdir;
226 StateDirectory = cfg.systemdStateDirectory;
227 RuntimeDirectory = cfg.systemdRuntimeDirectory;
228 RuntimeDirectoryPreserve = "yes";
229 };
230
231 unitConfig.RequiresMountsFor = cfg.dataDir;
232 };
233
234 # To be run manually because computationnally heavy
235 systemd.services.mastodon-cleanup-manual = {
236 description = "Cleanup mastodon";
237
238 environment.RAILS_ENV = "production";
239 environment.BUNDLE_PATH = "${cfg.workdir.gems}/${cfg.workdir.gems.ruby.gemPath}";
240 environment.BUNDLE_GEMFILE = "${cfg.workdir.gems.confFiles}/Gemfile";
241 environment.SOCKET = cfg.sockets.rails;
242
243 path = [ cfg.workdir.gems cfg.workdir.gems.ruby pkgs.file ];
244
245 script = ''
246 exec ./bin/tootctl statuses remove --days 365
247 '';
248
249 serviceConfig = {
250 User = cfg.user;
251 EnvironmentFile = cfg.configFile;
252 PrivateTmp = true;
253 Type = "oneshot";
254 WorkingDirectory = cfg.workdir;
255 StateDirectory = cfg.systemdStateDirectory;
256 RuntimeDirectory = cfg.systemdRuntimeDirectory;
257 RuntimeDirectoryPreserve = "yes";
258 };
259
260 unitConfig.RequiresMountsFor = cfg.dataDir;
261 };
262
263 systemd.services.mastodon-cleanup = {
264 description = "Cleanup mastodon";
265 startAt = "daily";
266 restartIfChanged = false;
267
268 environment.RAILS_ENV = "production";
269 environment.BUNDLE_PATH = "${cfg.workdir.gems}/${cfg.workdir.gems.ruby.gemPath}";
270 environment.BUNDLE_GEMFILE = "${cfg.workdir.gems.confFiles}/Gemfile";
271 environment.SOCKET = cfg.sockets.rails;
272
273 path = [ cfg.workdir.gems cfg.workdir.gems.ruby pkgs.file ];
274
275 script = ''
276 exec ./bin/tootctl media remove --days 30
277 '';
278
279 serviceConfig = {
280 User = cfg.user;
281 EnvironmentFile = cfg.configFile;
282 PrivateTmp = true;
283 Type = "oneshot";
284 WorkingDirectory = cfg.workdir;
285 StateDirectory = cfg.systemdStateDirectory;
286 RuntimeDirectory = cfg.systemdRuntimeDirectory;
287 RuntimeDirectoryPreserve = "yes";
288 };
289
290 unitConfig.RequiresMountsFor = cfg.dataDir;
291 };
292
293 systemd.services.mastodon-sidekiq = {
294 description = "Mastodon Sidekiq";
295 wantedBy = [ "multi-user.target" ];
296 after = [ "network.target" "mastodon-web.service" ];
297
298 environment.RAILS_ENV="production";
299 environment.BUNDLE_PATH = "${cfg.workdir.gems}/${cfg.workdir.gems.ruby.gemPath}";
300 environment.BUNDLE_GEMFILE = "${cfg.workdir.gems.confFiles}/Gemfile";
301 environment.DB_POOL="5";
302
303 path = [ cfg.workdir.gems cfg.workdir.gems.ruby pkgs.imagemagick pkgs.ffmpeg pkgs.file ];
304
305 script = ''
306 exec ./bin/bundle exec sidekiq -c 5 -q default -q mailers -q pull -q push
307 '';
308
309 serviceConfig = {
310 Slice = "mastodon.slice";
311 User = cfg.user;
312 EnvironmentFile = cfg.configFile;
313 PrivateTmp = true;
314 Restart = "always";
315 TimeoutSec = 15;
316 Type = "simple";
317 WorkingDirectory = cfg.workdir;
318 StateDirectory = cfg.systemdStateDirectory;
319 RuntimeDirectory = cfg.systemdRuntimeDirectory;
320 RuntimeDirectoryPreserve = "yes";
321 };
322
323 unitConfig.RequiresMountsFor = cfg.dataDir;
324 };
325
326 };
327 };
328 };
329 }
330
331