diff --git a/README.md b/README.md index ca44b42..efe0f40 100644 --- a/README.md +++ b/README.md @@ -107,15 +107,14 @@ The default php-fpm opcache configuration is to cache everything *forever* witho revalidation. Therefore, make sure to include `sudo systemctl reload phpfpm-${name}` in your deployment script. -To deploy your app, you can use -[ssh deployments](https://stancl.substack.com/i/170830424/setting-up-deployments), -rather than webhooks triggering pull hooks or other techniques. Since this module -creates a new user for each site, this deployment technique becomes non-problematic -and it's one of the simplest things you can do. Just ssh-keygen a private key, make a -GitHub Actions job use that on push, and include the public key in the site's `sshKeys` array. -Then, to be able to `git pull` the site on the server, add the user's `~/.ssh/id_ed25519.pub` -to the repository's deployment keys. The ssh key for the user is generated automatically -(can be disabled by setting `generateSshKey` to false). +To deploy your app, you can use ssh deployments, rather than webhooks triggering pull hooks +or other techniques. Since this module creates a new user for each site, this deployment +technique becomes non-problematic and it's one of the simplest things you can do. Just +ssh-keygen a private key, make a GitHub Actions job use that on push, and include the +public key in the site's `sshKeys` array. Then, to be able to `git pull` the site on the +server, add the user's `~/.ssh/id_ed25519.pub` to the repository's deployment keys. The +ssh key for the user is generated automatically (can be disabled by setting `generateSshKey` +to false). Also, if you're using `ssl` you should put this line into your system config: ```nix @@ -171,33 +170,15 @@ Simply `scp laravel.nix root@:/etc/nixos/` and start writing con ### www redirects -To redirect `www.acme.com` to `acme.com`, you can use the `wwwRedirect` attribute. It should be -null for no redirect, or an integer status code for an enabled redirect. +The module doesn't handle www redirects automatically. This may be added in the future. -```nix -(laravelSite { - name = "foo"; - domains = [ "foo.com" ] - wwwRedirect = 301; # permanent redirect - # ... -}) -``` - -With the config above, `www.foo.com/bar` will return a redirect to `foo.com/bar`, with the schema -matching the site's `ssl` config. +At this time, I'd recommend handling basic redirects like that on Cloudflare. ### Default nginx server Out of the box, if nginx cannot match an incoming request's host to a specific virtual host it will just use _some_ vhost. You can prevent behavior that by adding a module like this: -> [!NOTE] -> You can also use the `catchall.nix` module here (which includes the code below): -> -> `scp catchall.nix root@:/etc/nixos/` -> -> Then just add `./catchall.nix` to your modules array. - ```nix { services.nginx.virtualHosts."catchall" = { @@ -277,13 +258,6 @@ However a more proper solution is to use the `real_ip` module in common nginx co we can follow the [guide from the NixOS wiki](https://nixos.wiki/wiki/Nginx#Using_realIP_when_behind_CloudFlare_or_other_CDN). -> [!NOTE] -> You can also use the `realip.nix` module here (which wraps the code below): -> -> `scp realip.nix root@:/etc/nixos/` -> -> Then just add `./realip.nix` to your modules array. - ```nix # New module in your modules array { @@ -325,38 +299,6 @@ curl -s https://www.cloudflare.com/ips-v4 | sha256 | xargs nix hash convert --ha curl -s https://www.cloudflare.com/ips-v6 | sha256 | xargs nix hash convert --hash-algo sha256 --to nix32 ``` -## Static sites - -For hosting static sites, you can use `static.nix` very similarly to `laravel.nix`. Notable differences: -1. `root` is required, e.g. `name="foo"; root="build";` means `/srv/foo/build` will be served. In other - words, even though this is for static sites, we do not serve the entire `/srv/{name}` dir to allow - for version control and build steps. -2. By default, the `static-generic` user is used. Static sites do not always need strict user separation - since there's no request runtime. That said, the user is *very* limited and only has `pkgs.git` and - `pkgs.unzip`. Therefore it's only suited for static sites that are at most pulled from somewhere, - rather than built using Node.js. Also note that GitHub generally doesn't allow using a single SSH key - as the deploy key on multiple repos. For these reasons, it's still recommended to enable user creation - via `user = true;`. - -Full usage: -```nix -(staticSite { - name = "foo"; # name of the site - root = "build"; # directory within /srv/foo to be served by nginx - - user = true; # if false, static-generic is used. Default: false - domains = [ "foo.com" "bar.com" ]; # domains to serve the site on - ssl = true; # enableACME + forceSSL. Default: false - # Status code for www-to-non-www redirects. No redirect if null. Applies to all sites - wwwRedirect = 301; # Default: null - cloudflareOnly = true; # use Authenticated Origin Pulls. See the dedicated section. Default: false - extraPackages = [ pkgs.nodejs_24 ]; # only applies if user=true - generateSshKey = true; # defaults to true, used even with user=false - sshKeys = [ "array" "of" "public" "ssh" "keys" ]; # optional - extraNginxConfig = "nginx configuration string"; # optional -}) -``` - ## Maintenance It's a good idea to have `/etc/nixos` tracked in version control so you can easily revert the config @@ -381,11 +323,3 @@ Then clean garbage: ```sh sudo nix-collect-garbage -d ``` - -## Rebuilding - -From personal testing, running `nixos-rebuild switch` doesn't necessarily cause any downtime for users -if your website is behind Cloudflare. NixOS first builds everything it needs and only then, usually pretty -quickly, restarts (and adds, removes, etc) services as needed. This means your nginx **might** be down for -a very brief period, but if Cloudflare cannot connect to your server it will retry a couple of times. So at -most some requests will be very slightly delayed, but users should not see any errors on most rebuilds. diff --git a/catchall.nix b/catchall.nix deleted file mode 100644 index fec1160..0000000 --- a/catchall.nix +++ /dev/null @@ -1,7 +0,0 @@ -{ - services.nginx.virtualHosts."catchall" = { - default = true; - locations."/".return = "444"; - rejectSSL = true; - }; -} diff --git a/laravel.nix b/laravel.nix index 2b8825a..30f8bd6 100644 --- a/laravel.nix +++ b/laravel.nix @@ -3,7 +3,6 @@ phpPackage, # e.g. pkgs.php84 domains ? [], # e.g. [ "example.com" "acme.com" ] ssl ? false, # Should SSL be used - wwwRedirect ? null, # The status code used for www-to-non-www redirects. Null means no redirect cloudflareOnly ? false, # Should CF Authenticated Origin Pulls be used extraNginxConfig ? null, # Extra nginx config string sshKeys ? null, # SSH public keys used to log into the site's user for deployments @@ -32,7 +31,7 @@ { config, lib, pkgs, ... }: let - username = "laravel-${name}"; + mkUsername = siteName: "laravel-${siteName}"; in { services.nginx.enable = true; security.acme.acceptTerms = lib.mkIf ssl true; @@ -42,14 +41,13 @@ in { # Create welcome message for user # todo: the created /etc file should ideally be 0750 - # Note: keep in sync with static.nix environment.etc."laravel-${name}-bashrc".text = '' export PATH="$HOME/.config/composer/vendor/bin/:$PATH" # Laravel site welcome message echo "Welcome to ${name} Laravel site!" echo "Domains: ${lib.concatStringsSep ", " domains}" - echo "User home: /home/${username}" + echo "User home: /home/${mkUsername name}" echo "Site: /srv/${name}" echo "Restart php-fpm: sudo systemctl reload phpfpm-${name}" ${lib.optionalString queue ''echo "Restart queue: php artisan queue:restart"''} @@ -62,12 +60,12 @@ in { systemd.tmpfiles.rules = [ "d /srv 0751 root root - -" "d /home 0751 root root - -" - "d /srv/${name} 0750 ${username} ${username} - -" - "C /home/${username}/.bashrc 0640 ${username} ${username} - /etc/laravel-${name}-bashrc" + "d /srv/${name} 0750 ${mkUsername name} ${mkUsername name} - -" + "C /home/${mkUsername name}/.bashrc 0640 ${mkUsername name} ${mkUsername name} - /etc/laravel-${name}-bashrc" ]; services.cron.systemCronJobs = [ - "* * * * * ${username} cd /srv/${name} && ${phpPackage}/bin/php artisan schedule:run > /dev/null 2>&1" + "* * * * * ${mkUsername name} cd /srv/${name} && ${phpPackage}/bin/php artisan schedule:run > /dev/null 2>&1" ]; # Laravel queue worker service @@ -77,8 +75,8 @@ in { wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "simple"; - User = username; - Group = username; + User = mkUsername name; + Group = mkUsername name; WorkingDirectory = "/srv/${name}"; ExecStart = "${phpPackage}/bin/php artisan queue:work ${queueArgs}"; Restart = "always"; @@ -90,9 +88,8 @@ in { }; # SSH key generation for git deployments - # Note: keep in sync with static.nix systemd.services."generate-ssh-key-${name}" = lib.mkIf generateSshKey { - description = "Generate SSH key for ${username}"; + description = "Generate SSH key for ${mkUsername name}"; wantedBy = [ "multi-user.target" ]; after = [ "users.target" ]; serviceConfig = { @@ -101,15 +98,15 @@ in { User = "root"; }; script = '' - USER_HOME="/home/${username}" + USER_HOME="/home/${mkUsername name}" SSH_DIR="$USER_HOME/.ssh" KEY_FILE="$SSH_DIR/id_ed25519" if [[ ! -f "$KEY_FILE" ]]; then - echo "Generating SSH key for ${username}" + echo "Generating SSH key for ${mkUsername name}" mkdir -p "$SSH_DIR" - ${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f "$KEY_FILE" -N "" -C "${username}" - chown -R ${username}:${username} "$SSH_DIR" + ${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f "$KEY_FILE" -N "" -C "${mkUsername name}" + chown -R ${mkUsername name}:${mkUsername name} "$SSH_DIR" chmod 700 "$SSH_DIR" chmod 600 "$KEY_FILE" chmod 640 "$KEY_FILE.pub" @@ -117,7 +114,7 @@ in { echo "Public key for deploy key:" cat "$KEY_FILE.pub" else - echo "SSH key already exists for ${username}" + echo "SSH key already exists for ${mkUsername name}" fi ''; }; @@ -172,18 +169,11 @@ in { deny all; ''; }; - }) // lib.optionalAttrs (wwwRedirect != null) (lib.genAttrs (map (domain: "www.${domain}") domains) (wwwDomain: { - enableACME = ssl; - forceSSL = ssl; - - locations."/" = { - return = "${toString wwwRedirect} ${if ssl then "https" else "http"}://${lib.removePrefix "www." wwwDomain}$request_uri"; - }; - })); + }); # PHP-FPM pool configuration services.phpfpm.pools.${name} = { - user = username; + user = mkUsername name; phpPackage = phpPackage; settings = poolSettings // extraPoolSettings // { "listen.owner" = config.services.nginx.user; @@ -191,11 +181,11 @@ in { }; # User and group settings - users.users.${username} = { - group = username; + users.users.${mkUsername name} = { + group = mkUsername name; isSystemUser = true; createHome = true; - home = "/home/${username}"; + home = "/home/${mkUsername name}"; homeMode = "750"; shell = pkgs.bashInteractive; packages = [ phpPackage pkgs.git pkgs.unzip phpPackage.packages.composer ] ++ extraPackages; @@ -203,14 +193,14 @@ in { openssh.authorizedKeys.keys = sshKeys; }; - users.groups.${username} = {}; + users.groups.${mkUsername name} = {}; # Add site group to nginx service - systemd.services.nginx.serviceConfig.SupplementaryGroups = [ username ]; + systemd.services.nginx.serviceConfig.SupplementaryGroups = [ (mkUsername name) ]; # Sudo rules for service management security.sudo.extraRules = [{ - users = [ username ]; + users = [ (mkUsername name) ]; commands = [ { command = "/run/current-system/sw/bin/systemctl reload phpfpm-${name}"; diff --git a/realip.nix b/realip.nix deleted file mode 100644 index 4305e37..0000000 --- a/realip.nix +++ /dev/null @@ -1,20 +0,0 @@ -{ pkgs, lib, ... }: { - services.nginx.commonHttpConfig = - let - realIpsFromList = lib.strings.concatMapStringsSep "\n" (x: "set_real_ip_from ${x};"); - fileToList = x: lib.strings.splitString "\n" (builtins.readFile x); - cfipv4 = fileToList (pkgs.fetchurl { - url = "https://www.cloudflare.com/ips-v4"; - sha256 = "0ywy9sg7spafi3gm9q5wb59lbiq0swvf0q3iazl0maq1pj1nsb7h"; - }); - cfipv6 = fileToList (pkgs.fetchurl { - url = "https://www.cloudflare.com/ips-v6"; - sha256 = "1ad09hijignj6zlqvdjxv7rjj8567z357zfavv201b9vx3ikk7cy"; - }); - in - '' - ${realIpsFromList cfipv4} - ${realIpsFromList cfipv6} - real_ip_header CF-Connecting-IP; - ''; -} diff --git a/static.nix b/static.nix deleted file mode 100644 index 0a7e18c..0000000 --- a/static.nix +++ /dev/null @@ -1,132 +0,0 @@ -{ - name, # Name of the site, /srv/{name} will be based on this as well as the username if user=true - root, # The directory within /srv/{name} that should be served by nginx - user ? false, # Should a user be created. If false, static-generic is used - domains ? [], # e.g. [ "example.com" "acme.com" ] - ssl ? false, # Should SSL be used - wwwRedirect ? null, # The status code used for www-to-non-www redirects. Null means no redirect - cloudflareOnly ? false, # Should CF Authenticated Origin Pulls be used - extraNginxConfig ? null, # Extra nginx config string - sshKeys ? null, # SSH public keys used to log into the site's user for deployments - extraPackages ? [], # Any extra packages the user should have in $PATH (only used with user=true) - generateSshKey ? true, # Generate an SSH key for the user (used for GH deploy keys) - ... -}: - -{ lib, pkgs, ... }: -let - username = if user then "static-${name}" else "static-generic"; -in { - services.nginx.enable = true; - security.acme.acceptTerms = true; - networking.firewall.allowedTCPPorts = [ 80 ] ++ lib.optionals ssl [ 443 ]; - - services.nginx.virtualHosts = lib.genAttrs domains (domain: { - enableACME = ssl; - forceSSL = ssl; - root = "/srv/${name}/${root}"; - - extraConfig = '' - ${lib.optionalString cloudflareOnly '' - ssl_verify_client on; - ssl_client_certificate ${pkgs.fetchurl { - url = "https://developers.cloudflare.com/ssl/static/authenticated_origin_pull_ca.pem"; - sha256 = "0hxqszqfzsbmgksfm6k0gp0hsx9k1gqx24gakxqv0391wl6fsky1"; - }}; - ''} - ${lib.optionalString (extraNginxConfig != null) extraNginxConfig} - ''; - - locations = { - "= /favicon.ico".extraConfig = '' - access_log off; - log_not_found off; - ''; - - "= /robots.txt".extraConfig = '' - access_log off; - log_not_found off; - ''; - }; - }) // lib.optionalAttrs (wwwRedirect != null) (lib.genAttrs (map (domain: "www.${domain}") domains) (wwwDomain: { - enableACME = ssl; - forceSSL = ssl; - - locations."/" = { - return = "${toString wwwRedirect} ${if ssl then "https" else "http"}://${lib.removePrefix "www." wwwDomain}$request_uri"; - }; - })); - - systemd.tmpfiles.rules = [ - "d /srv 0751 root root - -" - "d /home 0751 root root - -" - "d /srv/${name} 0750 ${username} ${username} - -" - ] ++ lib.optional user - "C /home/${username}/.bashrc 0640 ${username} ${username} - /etc/static-${name}-bashrc"; - - # User and group settings - users.users.${username} = { - group = username; - isSystemUser = true; - createHome = true; - home = "/home/${username}"; - homeMode = "750"; - shell = pkgs.bashInteractive; - packages = [ pkgs.git pkgs.unzip ] ++ lib.optionals user extraPackages; - } // lib.optionalAttrs (sshKeys != null && user) { - openssh.authorizedKeys.keys = sshKeys; - }; - - users.groups.${username} = {}; - - # Add site group to nginx service - systemd.services.nginx.serviceConfig.SupplementaryGroups = [ username ]; - - # SSH key generation for git deployments - # Note: keep in sync with laravel.nix - # Unlike laravel.nix, the key here includes username, not the site name since static-generic - # can be used for multiple sites - systemd.services."generate-ssh-key-${username}" = lib.mkIf generateSshKey { - description = "Generate SSH key for ${username}"; - wantedBy = [ "multi-user.target" ]; - after = [ "users.target" ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - User = "root"; - }; - script = '' - USER_HOME="/home/${username}" - SSH_DIR="$USER_HOME/.ssh" - KEY_FILE="$SSH_DIR/id_ed25519" - - if [[ ! -f "$KEY_FILE" ]]; then - echo "Generating SSH key for ${username}" - mkdir -p "$SSH_DIR" - ${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f "$KEY_FILE" -N "" -C "${username}" - chown -R ${username}:${username} "$SSH_DIR" - chmod 700 "$SSH_DIR" - chmod 600 "$KEY_FILE" - chmod 640 "$KEY_FILE.pub" - echo "SSH key generated: $KEY_FILE.pub" - echo "Public key for deploy key:" - cat "$KEY_FILE.pub" - else - echo "SSH key already exists for ${username}" - fi - ''; - }; - - # Create welcome message for user - # Note: keep in sync with laravel.nix (same block, minor changes here) - environment.etc."static-${name}-bashrc" = lib.mkIf user { - text = '' - echo "Welcome to ${name} static site!" - echo "Domains: ${lib.concatStringsSep ", " domains}" - echo "User home: /home/${username}" - echo "Site: /srv/${name}" - ${lib.optionalString generateSshKey ''echo "SSH public key: cat ~/.ssh/id_ed25519.pub"''} - echo "---" - ''; - } ; -}