Compare commits
No commits in common. "dfa5327a2b609f4ab111631f547ead0080c81537" and "72cdd7dc4e3255d8c0359eb08c3017dcedbe776f" have entirely different histories.
dfa5327a2b
...
72cdd7dc4e
4 changed files with 42 additions and 447 deletions
21
flake.nix
21
flake.nix
|
|
@ -4,18 +4,10 @@
|
||||||
utils.url = "github:numtide/flake-utils";
|
utils.url = "github:numtide/flake-utils";
|
||||||
};
|
};
|
||||||
outputs = { self, nixpkgs, utils }:
|
outputs = { self, nixpkgs, utils }:
|
||||||
let
|
utils.lib.eachDefaultSystem (system:
|
||||||
perSystem = utils.lib.eachDefaultSystem (system:
|
|
||||||
let
|
let
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
in {
|
in {
|
||||||
packages = {
|
|
||||||
tide-backend = pkgs.callPackage ./nix/packages/backend.nix { };
|
|
||||||
tide-frontend = pkgs.callPackage ./nix/packages/frontend.nix {
|
|
||||||
apiUrl = "https://apitide.yisroelbaum.com";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
onefetch
|
onefetch
|
||||||
|
|
@ -58,15 +50,4 @@
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
in
|
|
||||||
perSystem // {
|
|
||||||
nixosModules.tide = import ./nix/module.nix { inherit self; };
|
|
||||||
|
|
||||||
overlays.default = final: prev: {
|
|
||||||
tide-backend = final.callPackage ./nix/packages/backend.nix { };
|
|
||||||
tide-frontend = final.callPackage ./nix/packages/frontend.nix {
|
|
||||||
apiUrl = "https://apitide.yisroelbaum.com";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
328
nix/module.nix
328
nix/module.nix
|
|
@ -1,328 +0,0 @@
|
||||||
{ self }:
|
|
||||||
{ 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";
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options.services.tide = {
|
|
||||||
enable = lib.mkEnableOption "TIDE blogging platform";
|
|
||||||
|
|
||||||
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.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
users.users.${cfg.user} = {
|
|
||||||
isSystemUser = true;
|
|
||||||
group = cfg.group;
|
|
||||||
home = "/var/lib/tide";
|
|
||||||
};
|
|
||||||
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} -"
|
|
||||||
];
|
|
||||||
|
|
||||||
# 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
|
|
||||||
rm -rf ${appRoot}
|
|
||||||
mkdir -p ${appRoot}
|
|
||||||
for entry in ${cfg.backendPackage}/share/php/tide-backend/*; do
|
|
||||||
name="$(basename "$entry")"
|
|
||||||
case "$name" in
|
|
||||||
storage|bootstrap)
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
ln -s "$entry" "${appRoot}/$name"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
# bootstrap dir: symlink everything from the store
|
|
||||||
# except cache/ which must be writable.
|
|
||||||
mkdir -p ${appRoot}/bootstrap
|
|
||||||
for entry in ${cfg.backendPackage}/share/php/tide-backend/bootstrap/*; do
|
|
||||||
name="$(basename "$entry")"
|
|
||||||
if [ "$name" != "cache" ]; then
|
|
||||||
ln -sf "$entry" "${appRoot}/bootstrap/$name"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
ln -sfn ${stateRoot}/bootstrap-cache ${appRoot}/bootstrap/cache
|
|
||||||
ln -sfn ${stateRoot}/storage ${appRoot}/storage
|
|
||||||
|
|
||||||
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;
|
|
||||||
phpEnv = {
|
|
||||||
APP_ENV = "production";
|
|
||||||
APP_DEBUG = "false";
|
|
||||||
APP_URL = "https://${cfg.apiDomain}";
|
|
||||||
LOG_CHANNEL = "stderr";
|
|
||||||
DB_CONNECTION = "pgsql";
|
|
||||||
DB_HOST = "/run/postgresql";
|
|
||||||
DB_PORT = "5432";
|
|
||||||
DB_DATABASE = cfg.database.name;
|
|
||||||
DB_USERNAME = cfg.database.user;
|
|
||||||
SESSION_DRIVER = "database";
|
|
||||||
CACHE_STORE = "database";
|
|
||||||
QUEUE_CONNECTION = "database";
|
|
||||||
};
|
|
||||||
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}" = {
|
|
||||||
after = [
|
|
||||||
"tide-prepare.service"
|
|
||||||
"postgresql.service"
|
|
||||||
];
|
|
||||||
requires = [
|
|
||||||
"tide-prepare.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;
|
|
||||||
};
|
|
||||||
environment = {
|
|
||||||
APP_ENV = "production";
|
|
||||||
APP_DEBUG = "false";
|
|
||||||
APP_URL = "https://${cfg.apiDomain}";
|
|
||||||
DB_CONNECTION = "pgsql";
|
|
||||||
DB_HOST = "/run/postgresql";
|
|
||||||
DB_PORT = "5432";
|
|
||||||
DB_DATABASE = cfg.database.name;
|
|
||||||
DB_USERNAME = cfg.database.user;
|
|
||||||
};
|
|
||||||
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
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
# 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";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
{ php84, lib }:
|
|
||||||
let
|
|
||||||
php = php84;
|
|
||||||
in
|
|
||||||
php.buildComposerProject (finalAttrs: {
|
|
||||||
pname = "tide-backend";
|
|
||||||
version = "0.1.0";
|
|
||||||
|
|
||||||
src = lib.cleanSource ../../backend;
|
|
||||||
|
|
||||||
composerNoDev = true;
|
|
||||||
composerNoPlugins = true;
|
|
||||||
composerNoScripts = true;
|
|
||||||
|
|
||||||
vendorHash = "sha256-OYpfX435tPJqiOzQPpWPXCVH1rTeQ74dGuNVyk6+c1A=";
|
|
||||||
|
|
||||||
passthru = {
|
|
||||||
inherit php;
|
|
||||||
};
|
|
||||||
|
|
||||||
meta = {
|
|
||||||
description = "TIDE Laravel backend";
|
|
||||||
license = lib.licenses.mit;
|
|
||||||
};
|
|
||||||
})
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
{ buildNpmPackage, lib, nodejs_22, apiUrl }:
|
|
||||||
buildNpmPackage (finalAttrs: {
|
|
||||||
pname = "tide-frontend";
|
|
||||||
version = "0.0.0";
|
|
||||||
|
|
||||||
src = lib.cleanSource ../../frontend/blog_portal;
|
|
||||||
|
|
||||||
nodejs = nodejs_22;
|
|
||||||
|
|
||||||
npmDepsFetcherVersion = 2;
|
|
||||||
npmDepsHash = "sha256-NHAo9Bvg80W2341yPaw97khUCJyr/7fyQFvhQFKWYnY=";
|
|
||||||
|
|
||||||
env = {
|
|
||||||
VITE_API_URL = apiUrl;
|
|
||||||
# Cypress' postinstall reaches out to download.cypress.io, which
|
|
||||||
# is not allowed inside the Nix sandbox. Cypress is only used
|
|
||||||
# for local E2E so skip the download during the production
|
|
||||||
# build.
|
|
||||||
CYPRESS_INSTALL_BINARY = "0";
|
|
||||||
};
|
|
||||||
|
|
||||||
installPhase = ''
|
|
||||||
runHook preInstall
|
|
||||||
mkdir -p $out
|
|
||||||
cp -r dist/* $out/
|
|
||||||
runHook postInstall
|
|
||||||
'';
|
|
||||||
|
|
||||||
meta = {
|
|
||||||
description = "TIDE Vue frontend (blog_portal)";
|
|
||||||
license = lib.licenses.mit;
|
|
||||||
};
|
|
||||||
})
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue