nixpkgs/nixos/modules/services/web-apps/misskey.nix
2024-08-06 12:24:15 +02:00

418 lines
15 KiB
Nix

{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.misskey;
settingsFormat = pkgs.formats.yaml { };
redisType = lib.types.submodule {
freeformType = lib.types.attrsOf settingsFormat.type;
options = {
host = lib.mkOption {
type = lib.types.str;
default = "localhost";
description = "The Redis host.";
};
port = lib.mkOption {
type = lib.types.port;
default = 6379;
description = "The Redis port.";
};
};
};
settings = lib.mkOption {
description = ''
Configuration for Misskey, see
[`example.yml`](https://github.com/misskey-dev/misskey/blob/develop/.config/example.yml)
for all supported options.
'';
type = lib.types.submodule {
freeformType = lib.types.attrsOf settingsFormat.type;
options = {
url = lib.mkOption {
type = lib.types.str;
example = "https://example.tld/";
description = ''
The final user-facing URL. Do not change after running Misskey for the first time.
This needs to match up with the configured reverse proxy and is automatically configured when using `services.misskey.reverseProxy`.
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 3000;
description = "The port your Misskey server should listen on.";
};
socket = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/path/to/misskey.sock";
description = "The UNIX socket your Misskey server should listen on.";
};
chmodSocket = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "777";
description = "The file access mode of the UNIX socket.";
};
db = lib.mkOption {
description = "Database settings.";
type = lib.types.submodule {
options = {
host = lib.mkOption {
type = lib.types.str;
default = "/var/run/postgresql";
example = "localhost";
description = "The PostgreSQL host.";
};
port = lib.mkOption {
type = lib.types.port;
default = 5432;
description = "The PostgreSQL port.";
};
db = lib.mkOption {
type = lib.types.str;
default = "misskey";
description = "The database name.";
};
user = lib.mkOption {
type = lib.types.str;
default = "misskey";
description = "The user used for database authentication.";
};
pass = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "The password used for database authentication.";
};
disableCache = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to disable caching queries.";
};
extra = lib.mkOption {
type = lib.types.nullOr (lib.types.attrsOf settingsFormat.type);
default = null;
example = {
ssl = true;
};
description = "Extra connection options.";
};
};
};
default = { };
};
redis = lib.mkOption {
type = redisType;
default = { };
description = "`ioredis` options. See [`README`](https://github.com/redis/ioredis?tab=readme-ov-file#connect-to-redis) for reference.";
};
redisForPubsub = lib.mkOption {
type = lib.types.nullOr redisType;
default = null;
description = "`ioredis` options for pubsub. See [`README`](https://github.com/redis/ioredis?tab=readme-ov-file#connect-to-redis) for reference.";
};
redisForJobQueue = lib.mkOption {
type = lib.types.nullOr redisType;
default = null;
description = "`ioredis` options for the job queue. See [`README`](https://github.com/redis/ioredis?tab=readme-ov-file#connect-to-redis) for reference.";
};
redisForTimelines = lib.mkOption {
type = lib.types.nullOr redisType;
default = null;
description = "`ioredis` options for timelines. See [`README`](https://github.com/redis/ioredis?tab=readme-ov-file#connect-to-redis) for reference.";
};
meilisearch = lib.mkOption {
description = "Meilisearch connection options.";
type = lib.types.nullOr (
lib.types.submodule {
options = {
host = lib.mkOption {
type = lib.types.str;
default = "localhost";
description = "The Meilisearch host.";
};
port = lib.mkOption {
type = lib.types.port;
default = 7700;
description = "The Meilisearch port.";
};
apiKey = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "The Meilisearch API key.";
};
ssl = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to connect via SSL.";
};
index = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Meilisearch index to use.";
};
scope = lib.mkOption {
type = lib.types.enum [
"local"
"global"
];
default = "local";
description = "The search scope.";
};
};
}
);
default = null;
};
id = lib.mkOption {
type = lib.types.enum [
"aid"
"aidx"
"meid"
"ulid"
"objectid"
];
default = "aidx";
description = "The ID generation method to use. Do not change after starting Misskey for the first time.";
};
};
};
};
in
{
options = {
services.misskey = {
enable = lib.mkEnableOption "misskey";
package = lib.mkPackageOption pkgs "misskey" { };
inherit settings;
database = {
createLocally = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Create the PostgreSQL database locally. Sets `services.misskey.settings.db.{db,host,port,user,pass}`.";
};
passwordFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "The path to a file containing the database password. Sets `services.misskey.settings.db.pass`.";
};
};
redis = {
createLocally = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Create and use a local Redis instance. Sets `services.misskey.settings.redis.host`.";
};
passwordFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "The path to a file containing the Redis password. Sets `services.misskey.settings.redis.pass`.";
};
};
meilisearch = {
createLocally = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Create and use a local Meilisearch instance. Sets `services.misskey.settings.meilisearch.{host,port,ssl}`.";
};
keyFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "The path to a file containing the Meilisearch API key. Sets `services.misskey.settings.meilisearch.apiKey`.";
};
};
reverseProxy = {
enable = lib.mkEnableOption "a HTTP reverse proxy for Misskey";
webserver = lib.mkOption {
type = lib.types.attrTag {
nginx = lib.mkOption {
type = lib.types.submodule (import ../web-servers/nginx/vhost-options.nix);
default = { };
description = ''
Extra configuration for the nginx virtual host of Misskey.
Set to `{ }` to use the default configuration.
'';
};
caddy = lib.mkOption {
type = lib.types.submodule (
import ../web-servers/caddy/vhost-options.nix { cfg = config.services.caddy; }
);
default = { };
description = ''
Extra configuration for the caddy virtual host of Misskey.
Set to `{ }` to use the default configuration.
'';
};
};
description = "The webserver to use as the reverse proxy.";
};
host = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = ''
The fully qualified domain name to bind to. Sets `services.misskey.settings.url`.
This is required when using `services.misskey.reverseProxy.enable = true`.
'';
example = "misskey.example.com";
default = null;
};
ssl = lib.mkOption {
type = lib.types.nullOr lib.types.bool;
description = ''
Whether to enable SSL for the reverse proxy. Sets `services.misskey.settings.url`.
This is required when using `services.misskey.reverseProxy.enable = true`.
'';
example = true;
default = null;
};
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion =
cfg.reverseProxy.enable -> ((cfg.reverseProxy.host != null) && (cfg.reverseProxy.ssl != null));
message = "`services.misskey.reverseProxy.enable` requires `services.misskey.reverseProxy.host` and `services.misskey.reverseProxy.ssl` to be set.";
}
];
services.misskey.settings = lib.mkMerge [
(lib.mkIf cfg.database.createLocally {
db = {
db = lib.mkDefault "misskey";
# Use unix socket instead of localhost to allow PostgreSQL peer authentication,
# required for `services.postgresql.ensureUsers`
host = lib.mkDefault "/var/run/postgresql";
port = lib.mkDefault config.services.postgresql.settings.port;
user = lib.mkDefault "misskey";
pass = lib.mkDefault null;
};
})
(lib.mkIf (cfg.database.passwordFile != null) { db.pass = lib.mkDefault "@DATABASE_PASSWORD@"; })
(lib.mkIf cfg.redis.createLocally { redis.host = lib.mkDefault "localhost"; })
(lib.mkIf (cfg.redis.passwordFile != null) { redis.pass = lib.mkDefault "@REDIS_PASSWORD@"; })
(lib.mkIf cfg.meilisearch.createLocally {
meilisearch = {
host = lib.mkDefault "localhost";
port = lib.mkDefault config.services.meilisearch.listenPort;
ssl = lib.mkDefault false;
};
})
(lib.mkIf (cfg.meilisearch.keyFile != null) {
meilisearch.apiKey = lib.mkDefault "@MEILISEARCH_KEY@";
})
(lib.mkIf cfg.reverseProxy.enable {
url = lib.mkDefault "${
if cfg.reverseProxy.ssl then "https" else "http"
}://${cfg.reverseProxy.host}";
})
];
systemd.services.misskey = {
after = [
"network-online.target"
"postgresql.service"
];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
MISSKEY_CONFIG_YML = "/run/misskey/default.yml";
};
preStart =
''
install -m 700 ${settingsFormat.generate "misskey-config.yml" cfg.settings} /run/misskey/default.yml
''
+ (lib.optionalString (cfg.database.passwordFile != null) ''
${pkgs.replace-secret}/bin/replace-secret '@DATABASE_PASSWORD@' "${cfg.database.passwordFile}" /run/misskey/default.yml
'')
+ (lib.optionalString (cfg.redis.passwordFile != null) ''
${pkgs.replace-secret}/bin/replace-secret '@REDIS_PASSWORD@' "${cfg.redis.passwordFile}" /run/misskey/default.yml
'')
+ (lib.optionalString (cfg.meilisearch.keyFile != null) ''
${pkgs.replace-secret}/bin/replace-secret '@MEILISEARCH_KEY@' "${cfg.meilisearch.keyFile}" /run/misskey/default.yml
'');
serviceConfig = {
ExecStart = "${cfg.package}/bin/misskey migrateandstart";
RuntimeDirectory = "misskey";
RuntimeDirectoryMode = "700";
StateDirectory = "misskey";
StateDirectoryMode = "700";
TimeoutSec = 60;
DynamicUser = true;
User = "misskey";
LockPersonality = true;
PrivateDevices = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectProc = "invisible";
ProtectKernelModules = true;
ProtectKernelTunables = true;
RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_NETLINK";
};
};
services.postgresql = lib.mkIf cfg.database.createLocally {
enable = true;
ensureDatabases = [ "misskey" ];
ensureUsers = [
{
name = "misskey";
ensureDBOwnership = true;
}
];
};
services.redis.servers = lib.mkIf cfg.redis.createLocally {
misskey = {
enable = true;
port = cfg.settings.redis.port;
};
};
services.meilisearch = lib.mkIf cfg.meilisearch.createLocally { enable = true; };
services.caddy = lib.mkIf (cfg.reverseProxy.enable && cfg.reverseProxy.webserver ? caddy) {
enable = true;
virtualHosts.${cfg.settings.url} = lib.mkMerge [
cfg.reverseProxy.webserver.caddy
{
hostName = lib.mkDefault cfg.settings.url;
extraConfig = ''
reverse_proxy localhost:${toString cfg.settings.port}
'';
}
];
};
services.nginx = lib.mkIf (cfg.reverseProxy.enable && cfg.reverseProxy.webserver ? nginx) {
enable = true;
virtualHosts.${cfg.reverseProxy.host} = lib.mkMerge [
cfg.reverseProxy.webserver.nginx
{
locations."/" = {
proxyPass = lib.mkDefault "http://localhost:${toString cfg.settings.port}";
proxyWebsockets = lib.mkDefault true;
recommendedProxySettings = lib.mkDefault true;
};
}
(lib.mkIf (cfg.reverseProxy.ssl != null) { forceSSL = lib.mkDefault cfg.reverseProxy.ssl; })
];
};
};
meta = {
maintainers = [ lib.maintainers.feathecutie ];
};
}