replace app with discourse module
This commit is contained in:
parent
63a68f8ae0
commit
95cdeaedfa
236 changed files with 357 additions and 31169 deletions
403
nix/module.nix
403
nix/module.nix
|
|
@ -1,301 +1,144 @@
|
|||
{ self }:
|
||||
{ config, lib, pkgs, ... }:
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.services.tide;
|
||||
|
||||
defaultBackend = self.packages.${pkgs.system}.tide-backend;
|
||||
defaultFrontend = self.packages.${pkgs.system}.tide-frontend.override {
|
||||
apiUrl = "https://${cfg.apiDomain}";
|
||||
};
|
||||
|
||||
# The Laravel package lives in the Nix store (read-only). Laravel
|
||||
# needs a writable storage/ and bootstrap/cache/. We materialize a
|
||||
# writable copy at /var/lib/tide/app whose contents are symlinks
|
||||
# back into the store, except for storage/ and bootstrap/cache/
|
||||
# which are real writable directories under /var/lib/tide/state.
|
||||
appRoot = "/var/lib/tide/app";
|
||||
stateRoot = "/var/lib/tide/state";
|
||||
|
||||
poolName = "tide";
|
||||
fpmSocket = "/run/phpfpm/${poolName}.sock";
|
||||
dumpCommand = lib.concatStringsSep " " [
|
||||
"${config.services.postgresql.package}/bin/pg_dump"
|
||||
"--format=custom"
|
||||
"--dbname=${config.services.discourse.database.name}"
|
||||
];
|
||||
secretFileOption =
|
||||
description:
|
||||
lib.mkOption {
|
||||
type = lib.types.path;
|
||||
inherit description;
|
||||
};
|
||||
in
|
||||
{
|
||||
options.services.tide = {
|
||||
enable = lib.mkEnableOption "TIDE blogging platform";
|
||||
enable = lib.mkEnableOption "Torah Im Derech Eretz forum";
|
||||
|
||||
domain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "tide.example.com";
|
||||
description = "Domain serving the Vue frontend.";
|
||||
};
|
||||
|
||||
apiDomain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "apitide.example.com";
|
||||
description = "Domain serving the Laravel backend API.";
|
||||
};
|
||||
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "tide";
|
||||
description = "Unix user the Laravel process runs as.";
|
||||
};
|
||||
|
||||
group = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "tide";
|
||||
description = "Unix group the Laravel process runs as.";
|
||||
};
|
||||
|
||||
database = {
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "tide";
|
||||
description = "PostgreSQL database name.";
|
||||
};
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "tide";
|
||||
description = "PostgreSQL role used by Laravel.";
|
||||
};
|
||||
};
|
||||
|
||||
secretsFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
description = ''
|
||||
Path to a file containing environment variables for the
|
||||
Laravel pool. Must define APP_KEY at minimum. Read at
|
||||
service start, never copied into the Nix store.
|
||||
'';
|
||||
};
|
||||
|
||||
backendPackage = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default = defaultBackend;
|
||||
defaultText = lib.literalExpression
|
||||
"self.packages.\${pkgs.system}.tide-backend";
|
||||
description = "The Laravel backend derivation.";
|
||||
};
|
||||
|
||||
frontendPackage = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default = defaultFrontend;
|
||||
defaultText = lib.literalExpression
|
||||
"self.packages.\${pkgs.system}.tide-frontend";
|
||||
description = "The Vue frontend derivation (built static dist).";
|
||||
};
|
||||
|
||||
nginx = {
|
||||
useACMEHost = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "example.com";
|
||||
description = ''
|
||||
Reuse an existing wildcard ACME cert (set this to the
|
||||
apex domain whose cert covers both subdomains). When
|
||||
null, each vhost requests its own cert via
|
||||
enableACME = true.
|
||||
'';
|
||||
};
|
||||
secretFiles = {
|
||||
adminPassword = secretFileOption "Discourse administrator password file.";
|
||||
mailPassword = secretFileOption "Mailjet SMTP password file.";
|
||||
secretKeyBase = secretFileOption "Discourse secret key base file.";
|
||||
borgPassphrase = secretFileOption "Borg repository passphrase file.";
|
||||
borgPrivateKey = secretFileOption "Borg repository SSH private key file.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
users.users.${cfg.user} = {
|
||||
isSystemUser = true;
|
||||
group = cfg.group;
|
||||
home = "/var/lib/tide";
|
||||
services.discourse = {
|
||||
enable = true;
|
||||
admin = {
|
||||
email = "yisroel.d.baum@gmail.com";
|
||||
fullName = "Yisroel Baum";
|
||||
passwordFile = cfg.secretFiles.adminPassword;
|
||||
username = "yisroeldbaum";
|
||||
};
|
||||
database.ignorePostgresqlVersion = true;
|
||||
hostname = "discourse.torahimderecheretz.com";
|
||||
mail = {
|
||||
notificationEmailAddress = "system@torahimderecheretz.com";
|
||||
contactEmailAddress = "system@torahimderecheretz.com";
|
||||
|
||||
outgoing = {
|
||||
serverAddress = "in-v3.mailjet.com";
|
||||
port = 587;
|
||||
username = "1ebdd73d944240c1a9c3201e52a1e8c6";
|
||||
passwordFile = cfg.secretFiles.mailPassword;
|
||||
authentication = "login";
|
||||
};
|
||||
};
|
||||
secretKeyBaseFile = cfg.secretFiles.secretKeyBase;
|
||||
siteSettings.required = {
|
||||
title = "Torah Im Derech Eretz";
|
||||
site_description = "A forum to discuss the ideas of Rav Shamshon Refael Hirsch";
|
||||
};
|
||||
};
|
||||
|
||||
services.borgbackup.jobs.discourse = {
|
||||
paths = [
|
||||
"/var/lib/discourse"
|
||||
"/var/backup/discourse/discourse.dump"
|
||||
];
|
||||
repo = "ssh://oas17j8p@oas17j8p.repo.borgbase.com/./repo";
|
||||
user = "root";
|
||||
group = "root";
|
||||
encryption = {
|
||||
mode = "repokey-blake2";
|
||||
passCommand = "${pkgs.coreutils}/bin/cat ${cfg.secretFiles.borgPassphrase}";
|
||||
};
|
||||
doInit = true;
|
||||
compression = "auto,zstd";
|
||||
startAt = "*-*-* 07:15:00";
|
||||
persistentTimer = true;
|
||||
prune.keep = {
|
||||
daily = 7;
|
||||
weekly = 4;
|
||||
monthly = 6;
|
||||
};
|
||||
readWritePaths = [ "/var/backup/discourse" ];
|
||||
environment.BORG_RSH = lib.concatStringsSep " " [
|
||||
"ssh"
|
||||
"-i ${cfg.secretFiles.borgPrivateKey}"
|
||||
"-o IdentitiesOnly=yes"
|
||||
"-o BatchMode=yes"
|
||||
"-o StrictHostKeyChecking=accept-new"
|
||||
"-o UserKnownHostsFile=/root/.config/borg/known_hosts"
|
||||
];
|
||||
preHook = ''
|
||||
discourseWasActive=/root/.cache/borg/discourse-was-active
|
||||
dumpFile=/var/backup/discourse/discourse.dump
|
||||
|
||||
if ${pkgs.systemd}/bin/systemctl is-active --quiet discourse.service; then
|
||||
touch "$discourseWasActive"
|
||||
${pkgs.systemd}/bin/systemctl stop discourse.service
|
||||
else
|
||||
rm -f "$discourseWasActive"
|
||||
fi
|
||||
|
||||
rm -f "$dumpFile"
|
||||
umask 077
|
||||
${lib.getExe' pkgs.su "su"} \
|
||||
-s ${pkgs.runtimeShell} \
|
||||
${config.services.postgresql.superUser} \
|
||||
-c '${dumpCommand}' \
|
||||
> "$dumpFile"
|
||||
'';
|
||||
postHook = ''
|
||||
discourseWasActive=/root/.cache/borg/discourse-was-active
|
||||
dumpFile=/var/backup/discourse/discourse.dump
|
||||
|
||||
rm -f "$dumpFile"
|
||||
|
||||
if [ -e "$discourseWasActive" ]; then
|
||||
${pkgs.systemd}/bin/systemctl start discourse.service
|
||||
rm -f "$discourseWasActive"
|
||||
fi
|
||||
'';
|
||||
};
|
||||
users.groups.${cfg.group} = { };
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /var/lib/tide 0750 ${cfg.user} ${cfg.group} -"
|
||||
"d ${stateRoot} 0750 ${cfg.user} ${cfg.group} -"
|
||||
"d ${stateRoot}/storage 0750 ${cfg.user} ${cfg.group} -"
|
||||
"d ${stateRoot}/storage/app 0750 ${cfg.user} ${cfg.group} -"
|
||||
"d ${stateRoot}/storage/app/public 0750 ${cfg.user} ${cfg.group} -"
|
||||
"d ${stateRoot}/storage/framework 0750 ${cfg.user} ${cfg.group} -"
|
||||
"d ${stateRoot}/storage/framework/cache 0750 ${cfg.user} ${cfg.group} -"
|
||||
"d ${stateRoot}/storage/framework/sessions 0750 ${cfg.user} ${cfg.group} -"
|
||||
"d ${stateRoot}/storage/framework/views 0750 ${cfg.user} ${cfg.group} -"
|
||||
"d ${stateRoot}/storage/framework/testing 0750 ${cfg.user} ${cfg.group} -"
|
||||
"d ${stateRoot}/storage/logs 0750 ${cfg.user} ${cfg.group} -"
|
||||
"d ${stateRoot}/bootstrap-cache 0750 ${cfg.user} ${cfg.group} -"
|
||||
"d /var/backup/discourse 0700 root root - -"
|
||||
];
|
||||
|
||||
# Materialize the writable app root by symlinking from the
|
||||
# store package, then redirecting the two mutable subtrees
|
||||
# to /var/lib/tide/state.
|
||||
systemd.services.tide-prepare = {
|
||||
description = "Prepare TIDE Laravel app root";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
before = [
|
||||
"phpfpm-${poolName}.service"
|
||||
"tide-migrate.service"
|
||||
];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = ''
|
||||
set -eu
|
||||
|
||||
# Copy the immutable store package into a writable app root.
|
||||
# Symlinking would defeat Laravel's __DIR__ based path
|
||||
# resolution: PHP's __DIR__ follows symlinks back to the
|
||||
# read only store, breaking storage/ and bootstrap/cache/
|
||||
# writes even when those subtrees are themselves symlinked
|
||||
# into a writable state dir.
|
||||
rm -rf ${appRoot}
|
||||
mkdir -p ${appRoot}
|
||||
cp -rL ${cfg.backendPackage}/share/php/tide-backend/. ${appRoot}/
|
||||
chmod -R u+w ${appRoot}
|
||||
|
||||
# Redirect the two mutable subtrees to /var/lib/tide/state
|
||||
# so they survive activations and don't accumulate inside
|
||||
# the disposable app root copy.
|
||||
rm -rf ${appRoot}/storage ${appRoot}/bootstrap/cache
|
||||
ln -sfn ${stateRoot}/storage ${appRoot}/storage
|
||||
ln -sfn ${stateRoot}/bootstrap-cache ${appRoot}/bootstrap/cache
|
||||
|
||||
chown -R ${cfg.user}:${cfg.group} ${appRoot} ${stateRoot}
|
||||
'';
|
||||
};
|
||||
|
||||
# Postgres
|
||||
services.postgresql = {
|
||||
enable = true;
|
||||
ensureDatabases = [ cfg.database.name ];
|
||||
ensureUsers = [
|
||||
{
|
||||
name = cfg.database.user;
|
||||
ensureDBOwnership = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
# PHP-FPM pool
|
||||
services.phpfpm.pools.${poolName} = {
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
phpPackage = cfg.backendPackage.passthru.php;
|
||||
settings = {
|
||||
"listen.owner" = config.services.nginx.user;
|
||||
"listen.group" = config.services.nginx.group;
|
||||
"listen.mode" = "0660";
|
||||
"pm" = "dynamic";
|
||||
"pm.max_children" = 16;
|
||||
"pm.start_servers" = 2;
|
||||
"pm.min_spare_servers" = 2;
|
||||
"pm.max_spare_servers" = 4;
|
||||
"pm.max_requests" = 500;
|
||||
"catch_workers_output" = true;
|
||||
"decorate_workers_output" = false;
|
||||
"clear_env" = false;
|
||||
};
|
||||
};
|
||||
|
||||
# Pull APP_KEY, DB_PASSWORD, MAIL_*, etc. from the secrets
|
||||
# file at start time.
|
||||
systemd.services."phpfpm-${poolName}" = {
|
||||
systemd.services.borgbackup-job-discourse = {
|
||||
after = [
|
||||
"tide-prepare.service"
|
||||
"postgresql.service"
|
||||
"sops-install-secrets.service"
|
||||
"network-online.target"
|
||||
"postgresql.target"
|
||||
"discourse-postgresql.service"
|
||||
];
|
||||
requires = [
|
||||
"tide-prepare.service"
|
||||
wants = [
|
||||
"network-online.target"
|
||||
"postgresql.target"
|
||||
"discourse-postgresql.service"
|
||||
];
|
||||
serviceConfig = {
|
||||
EnvironmentFile = cfg.secretsFile;
|
||||
};
|
||||
};
|
||||
|
||||
# One-shot migrations + cache warm.
|
||||
systemd.services.tide-migrate = {
|
||||
description = "Run TIDE Laravel migrations and cache warm";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [
|
||||
"tide-prepare.service"
|
||||
"postgresql.service"
|
||||
];
|
||||
requires = [
|
||||
"tide-prepare.service"
|
||||
"postgresql.service"
|
||||
];
|
||||
before = [ "phpfpm-${poolName}.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
EnvironmentFile = cfg.secretsFile;
|
||||
WorkingDirectory = appRoot;
|
||||
};
|
||||
script = ''
|
||||
${cfg.backendPackage.passthru.php}/bin/php artisan migrate --force --no-interaction
|
||||
${cfg.backendPackage.passthru.php}/bin/php artisan storage:link --force || true
|
||||
${cfg.backendPackage.passthru.php}/bin/php artisan config:cache
|
||||
${cfg.backendPackage.passthru.php}/bin/php artisan route:cache
|
||||
'';
|
||||
};
|
||||
|
||||
users.users.${config.services.nginx.user}.extraGroups = [ cfg.group ];
|
||||
|
||||
# nginx vhosts
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts = {
|
||||
"${cfg.apiDomain}" =
|
||||
let
|
||||
sslAttrs =
|
||||
if cfg.nginx.useACMEHost != null then {
|
||||
forceSSL = true;
|
||||
useACMEHost = cfg.nginx.useACMEHost;
|
||||
} else {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
};
|
||||
in
|
||||
sslAttrs // {
|
||||
root = "${appRoot}/public";
|
||||
locations = {
|
||||
"/" = {
|
||||
tryFiles = "$uri $uri/ /index.php?$query_string";
|
||||
};
|
||||
"~ \\.php$" = {
|
||||
extraConfig = ''
|
||||
fastcgi_pass unix:${fpmSocket};
|
||||
fastcgi_index index.php;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
include ${pkgs.nginx}/conf/fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
fastcgi_param HTTPS on;
|
||||
'';
|
||||
};
|
||||
"~ /\\.ht" = {
|
||||
extraConfig = "deny all;";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
"${cfg.domain}" =
|
||||
let
|
||||
sslAttrs =
|
||||
if cfg.nginx.useACMEHost != null then {
|
||||
forceSSL = true;
|
||||
useACMEHost = cfg.nginx.useACMEHost;
|
||||
} else {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
};
|
||||
in
|
||||
sslAttrs // {
|
||||
root = "${cfg.frontendPackage}";
|
||||
locations."/" = {
|
||||
tryFiles = "$uri $uri/ /index.html";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue