diff --git a/hosts/profiles/ghost/default.nix b/hosts/profiles/ghost/default.nix new file mode 100644 index 00000000..6ea900cc --- /dev/null +++ b/hosts/profiles/ghost/default.nix @@ -0,0 +1,18 @@ +{pkgs, ... }: +{ + services.nginx = { + enable = true; + recommendedGzipSettings = true; + recommendedOptimisation = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + }; + + # Enables MySQL + services.mysql = { + enable = true; + package = pkgs.mariadb; + }; + + services."write.sealight.xyz".enable = true; +} diff --git a/modules/nixos/ghost.nix b/modules/nixos/ghost.nix new file mode 100644 index 00000000..f4fed915 --- /dev/null +++ b/modules/nixos/ghost.nix @@ -0,0 +1,148 @@ +{ options, lib, config, pkgs, ... }: +let + # domain for the Ghost blog + serverName = "write.sealight.xyz"; + # port on which the Ghost service runs + port = 1357; + # user used to run the Ghost service + userName = builtins.replaceStrings [ "." ] [ "_" ] serverName; + # MySQL database used by Ghost + dbName = userName; + # MySQL user used by Ghost + dbUser = userName; + # directory used to save the blog content + dataDir = "/var/lib/${userName}"; + # Ghost package we created in the section above + ghost = import ./ghost { inherit pkgs; }; + # script that sets up the Ghost content directory + setupScript = pkgs.writeScript "${serverName}-setup.sh" '' + #! ${pkgs.stdenv.shell} -e + chmod g+s "${dataDir}" + [[ ! -d "${dataDir}/content" ]] && cp -r "${ghost}/content" "${dataDir}/content" + chown -R "${userName}":"${userName}" "${dataDir}/content" + chmod -R +w "${dataDir}/content" + ln -f -s "/etc/${serverName}.json" "${dataDir}/config.production.json" + [[ -d "${dataDir}/current" ]] && rm "${dataDir}/current" + ln -f -s "${ghost}/current" "${dataDir}/current" + [[ -d "${dataDir}/content/themes/casper" ]] && rm "${dataDir}/content/themes/casper" + ln -f -s "${ghost}/current/content/themes/casper" "${dataDir}/content/themes/casper" + ''; + + databaseService = "mysql.service"; + + serviceConfig = config.services."${serverName}"; + options = { enable = lib.mkEnableOption "${serverName} service"; }; +in { + options.services.${serverName} = options; + config = lib.mkIf serviceConfig.enable { + # Creates the user and group + users.users.${userName} = { + isSystemUser = true; + group = userName; + createHome = true; + home = dataDir; + }; + users.groups.${userName} = { }; + + # Creates the Ghost config + environment.etc."${serverName}.json".text = '' + { + "url": "https://${serverName}", + "server": { + "port": ${port}, + "host": "0.0.0.0" + }, + "database": { + "client": "mysql", + "connection": { + "host": "localhost", + "user": "${dbUser}", + "database": "${dbName}", + "password": "", + "socketPath": "/run/mysqld/mysqld.sock" + } + }, + "mail": { + "transport": "sendmail" + }, + "logging": { + "transports": ["stdout"] + }, + "paths": { + "contentPath": "${dataDir}/content" + } + } + ''; + + # Sets up the Systemd service + systemd.services."${serverName}" = { + enable = true; + description = "${serverName} ghost blog"; + restartIfChanged = true; + restartTriggers = + [ ghost config.environment.etc."${serverName}.json".source ]; + requires = [ databaseService ]; + after = [ databaseService ]; + path = [ pkgs.nodejs pkgs.vips ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + User = userName; + Group = userName; + WorkingDirectory = dataDir; + # Executes the setup script before start + ExecStartPre = setupScript; + # Runs Ghost with node + ExecStart = "${pkgs.nodejs}/bin/node current/index.js"; + # Sandboxes the Systemd service + AmbientCapabilities = [ ]; + CapabilityBoundingSet = [ ]; + KeyringMode = "private"; + LockPersonality = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateMounts = true; + PrivateTmp = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectSystem = "full"; + RemoveIPC = true; + RestrictAddressFamilies = [ ]; + RestrictNamespaces = true; + RestrictRealtime = true; + }; + environment = { NODE_ENV = "production"; }; + }; + + # Sets up the blog virtual host on NGINX + services.nginx.virtualHosts.${serverName} = { + # Sets up Lets Encrypt SSL certificates for the blog + forceSSL = true; + enableACME = true; + locations."/" = { proxyPass = "http://127.0.0.1:${toString port}"; }; + extraConfig = '' + charset UTF-8; + + add_header Strict-Transport-Security "max-age=2592000; includeSubDomains" always; + add_header Referrer-Policy "strict-origin-when-cross-origin"; + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-XSS-Protection "1; mode=block"; + add_header X-Content-Type-Options nosniff; + ''; + }; + + # Sets up MySQL database and user for Ghost + services.mysql = { + ensureDatabases = [ dbName ]; + ensureUsers = [{ + name = dbUser; + ensurePermissions = { "${dbName}.*" = "ALL PRIVILEGES"; }; + }]; + }; + }; +} + diff --git a/pkgs/ghost/builder.sh b/pkgs/ghost/builder.sh new file mode 100644 index 00000000..e30d1015 --- /dev/null +++ b/pkgs/ghost/builder.sh @@ -0,0 +1,9 @@ +source "$stdenv"/setup + +export HOME=$(mktemp -d) +npm install --loglevel=info --logs-max=0 "ghost-cli@$ghostCliVersion" + +mkdir --parents "$out"/ +node_modules/ghost-cli/bin/ghost install "$version" --db=sqlite3 \ + --no-enable --no-prompt --no-stack --no-setup --no-start --dir "$out" + diff --git a/pkgs/ghost/default.nix b/pkgs/ghost/default.nix new file mode 100644 index 00000000..52ac7933 --- /dev/null +++ b/pkgs/ghost/default.nix @@ -0,0 +1,11 @@ +{ pkgs }: + +let + pname = "ghost"; + version = "5.8.0"; +in pkgs.stdenv.mkDerivation { + inherit pname version; + buildInputs = with pkgs; [ nodejs yarn vips ]; + ghostCliVersion = "1.21.1"; + builder = ./builder.sh; +}