1
0
Fork 0
mirror of https://github.com/archtechx/nix.git synced 2025-12-12 11:24:04 +00:00

Compare commits

..

5 commits

5 changed files with 266 additions and 31 deletions

View file

@ -107,14 +107,15 @@ The default php-fpm opcache configuration is to cache everything *forever* witho
revalidation. Therefore, make sure to include `sudo systemctl reload phpfpm-${name}` in revalidation. Therefore, make sure to include `sudo systemctl reload phpfpm-${name}` in
your deployment script. your deployment script.
To deploy your app, you can use ssh deployments, rather than webhooks triggering pull hooks To deploy your app, you can use
or other techniques. Since this module creates a new user for each site, this deployment [ssh deployments](https://stancl.substack.com/i/170830424/setting-up-deployments),
technique becomes non-problematic and it's one of the simplest things you can do. Just rather than webhooks triggering pull hooks or other techniques. Since this module
ssh-keygen a private key, make a GitHub Actions job use that on push, and include the creates a new user for each site, this deployment technique becomes non-problematic
public key in the site's `sshKeys` array. Then, to be able to `git pull` the site on the and it's one of the simplest things you can do. Just ssh-keygen a private key, make a
server, add the user's `~/.ssh/id_ed25519.pub` to the repository's deployment keys. The GitHub Actions job use that on push, and include the public key in the site's `sshKeys` array.
ssh key for the user is generated automatically (can be disabled by setting `generateSshKey` Then, to be able to `git pull` the site on the server, add the user's `~/.ssh/id_ed25519.pub`
to false). 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: Also, if you're using `ssl` you should put this line into your system config:
```nix ```nix
@ -170,15 +171,33 @@ Simply `scp laravel.nix root@<your server ip>:/etc/nixos/` and start writing con
### www redirects ### www redirects
The module doesn't handle www redirects automatically. This may be added in the future. 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.
At this time, I'd recommend handling basic redirects like that on Cloudflare. ```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.
### Default nginx server ### Default nginx server
Out of the box, if nginx cannot match an incoming request's host to a specific virtual host it will 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: 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@<server ip>:/etc/nixos/`
>
> Then just add `./catchall.nix` to your modules array.
```nix ```nix
{ {
services.nginx.virtualHosts."catchall" = { services.nginx.virtualHosts."catchall" = {
@ -258,6 +277,13 @@ However a more proper solution is to use the `real_ip` module in common nginx co
we can follow the [guide from the NixOS we can follow the [guide from the NixOS
wiki](https://nixos.wiki/wiki/Nginx#Using_realIP_when_behind_CloudFlare_or_other_CDN). 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@<server ip>:/etc/nixos/`
>
> Then just add `./realip.nix` to your modules array.
```nix ```nix
# New module in your modules array # New module in your modules array
{ {
@ -299,6 +325,38 @@ 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 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 ## Maintenance
It's a good idea to have `/etc/nixos` tracked in version control so you can easily revert the config It's a good idea to have `/etc/nixos` tracked in version control so you can easily revert the config
@ -323,3 +381,11 @@ Then clean garbage:
```sh ```sh
sudo nix-collect-garbage -d 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.

7
catchall.nix Normal file
View file

@ -0,0 +1,7 @@
{
services.nginx.virtualHosts."catchall" = {
default = true;
locations."/".return = "444";
rejectSSL = true;
};
}

View file

@ -3,6 +3,7 @@
phpPackage, # e.g. pkgs.php84 phpPackage, # e.g. pkgs.php84
domains ? [], # e.g. [ "example.com" "acme.com" ] domains ? [], # e.g. [ "example.com" "acme.com" ]
ssl ? false, # Should SSL be used 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 cloudflareOnly ? false, # Should CF Authenticated Origin Pulls be used
extraNginxConfig ? null, # Extra nginx config string extraNginxConfig ? null, # Extra nginx config string
sshKeys ? null, # SSH public keys used to log into the site's user for deployments sshKeys ? null, # SSH public keys used to log into the site's user for deployments
@ -31,7 +32,7 @@
{ config, lib, pkgs, ... }: { config, lib, pkgs, ... }:
let let
mkUsername = siteName: "laravel-${siteName}"; username = "laravel-${name}";
in { in {
services.nginx.enable = true; services.nginx.enable = true;
security.acme.acceptTerms = lib.mkIf ssl true; security.acme.acceptTerms = lib.mkIf ssl true;
@ -41,13 +42,14 @@ in {
# Create welcome message for user # Create welcome message for user
# todo: the created /etc file should ideally be 0750 # todo: the created /etc file should ideally be 0750
# Note: keep in sync with static.nix
environment.etc."laravel-${name}-bashrc".text = '' environment.etc."laravel-${name}-bashrc".text = ''
export PATH="$HOME/.config/composer/vendor/bin/:$PATH" export PATH="$HOME/.config/composer/vendor/bin/:$PATH"
# Laravel site welcome message # Laravel site welcome message
echo "Welcome to ${name} Laravel site!" echo "Welcome to ${name} Laravel site!"
echo "Domains: ${lib.concatStringsSep ", " domains}" echo "Domains: ${lib.concatStringsSep ", " domains}"
echo "User home: /home/${mkUsername name}" echo "User home: /home/${username}"
echo "Site: /srv/${name}" echo "Site: /srv/${name}"
echo "Restart php-fpm: sudo systemctl reload phpfpm-${name}" echo "Restart php-fpm: sudo systemctl reload phpfpm-${name}"
${lib.optionalString queue ''echo "Restart queue: php artisan queue:restart"''} ${lib.optionalString queue ''echo "Restart queue: php artisan queue:restart"''}
@ -60,12 +62,12 @@ in {
systemd.tmpfiles.rules = [ systemd.tmpfiles.rules = [
"d /srv 0751 root root - -" "d /srv 0751 root root - -"
"d /home 0751 root root - -" "d /home 0751 root root - -"
"d /srv/${name} 0750 ${mkUsername name} ${mkUsername name} - -" "d /srv/${name} 0750 ${username} ${username} - -"
"C /home/${mkUsername name}/.bashrc 0640 ${mkUsername name} ${mkUsername name} - /etc/laravel-${name}-bashrc" "C /home/${username}/.bashrc 0640 ${username} ${username} - /etc/laravel-${name}-bashrc"
]; ];
services.cron.systemCronJobs = [ services.cron.systemCronJobs = [
"* * * * * ${mkUsername name} cd /srv/${name} && ${phpPackage}/bin/php artisan schedule:run > /dev/null 2>&1" "* * * * * ${username} cd /srv/${name} && ${phpPackage}/bin/php artisan schedule:run > /dev/null 2>&1"
]; ];
# Laravel queue worker service # Laravel queue worker service
@ -75,8 +77,8 @@ in {
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
serviceConfig = { serviceConfig = {
Type = "simple"; Type = "simple";
User = mkUsername name; User = username;
Group = mkUsername name; Group = username;
WorkingDirectory = "/srv/${name}"; WorkingDirectory = "/srv/${name}";
ExecStart = "${phpPackage}/bin/php artisan queue:work ${queueArgs}"; ExecStart = "${phpPackage}/bin/php artisan queue:work ${queueArgs}";
Restart = "always"; Restart = "always";
@ -88,8 +90,9 @@ in {
}; };
# SSH key generation for git deployments # SSH key generation for git deployments
# Note: keep in sync with static.nix
systemd.services."generate-ssh-key-${name}" = lib.mkIf generateSshKey { systemd.services."generate-ssh-key-${name}" = lib.mkIf generateSshKey {
description = "Generate SSH key for ${mkUsername name}"; description = "Generate SSH key for ${username}";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
after = [ "users.target" ]; after = [ "users.target" ];
serviceConfig = { serviceConfig = {
@ -98,15 +101,15 @@ in {
User = "root"; User = "root";
}; };
script = '' script = ''
USER_HOME="/home/${mkUsername name}" USER_HOME="/home/${username}"
SSH_DIR="$USER_HOME/.ssh" SSH_DIR="$USER_HOME/.ssh"
KEY_FILE="$SSH_DIR/id_ed25519" KEY_FILE="$SSH_DIR/id_ed25519"
if [[ ! -f "$KEY_FILE" ]]; then if [[ ! -f "$KEY_FILE" ]]; then
echo "Generating SSH key for ${mkUsername name}" echo "Generating SSH key for ${username}"
mkdir -p "$SSH_DIR" mkdir -p "$SSH_DIR"
${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f "$KEY_FILE" -N "" -C "${mkUsername name}" ${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f "$KEY_FILE" -N "" -C "${username}"
chown -R ${mkUsername name}:${mkUsername name} "$SSH_DIR" chown -R ${username}:${username} "$SSH_DIR"
chmod 700 "$SSH_DIR" chmod 700 "$SSH_DIR"
chmod 600 "$KEY_FILE" chmod 600 "$KEY_FILE"
chmod 640 "$KEY_FILE.pub" chmod 640 "$KEY_FILE.pub"
@ -114,7 +117,7 @@ in {
echo "Public key for deploy key:" echo "Public key for deploy key:"
cat "$KEY_FILE.pub" cat "$KEY_FILE.pub"
else else
echo "SSH key already exists for ${mkUsername name}" echo "SSH key already exists for ${username}"
fi fi
''; '';
}; };
@ -169,11 +172,18 @@ in {
deny all; 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 # PHP-FPM pool configuration
services.phpfpm.pools.${name} = { services.phpfpm.pools.${name} = {
user = mkUsername name; user = username;
phpPackage = phpPackage; phpPackage = phpPackage;
settings = poolSettings // extraPoolSettings // { settings = poolSettings // extraPoolSettings // {
"listen.owner" = config.services.nginx.user; "listen.owner" = config.services.nginx.user;
@ -181,11 +191,11 @@ in {
}; };
# User and group settings # User and group settings
users.users.${mkUsername name} = { users.users.${username} = {
group = mkUsername name; group = username;
isSystemUser = true; isSystemUser = true;
createHome = true; createHome = true;
home = "/home/${mkUsername name}"; home = "/home/${username}";
homeMode = "750"; homeMode = "750";
shell = pkgs.bashInteractive; shell = pkgs.bashInteractive;
packages = [ phpPackage pkgs.git pkgs.unzip phpPackage.packages.composer ] ++ extraPackages; packages = [ phpPackage pkgs.git pkgs.unzip phpPackage.packages.composer ] ++ extraPackages;
@ -193,14 +203,14 @@ in {
openssh.authorizedKeys.keys = sshKeys; openssh.authorizedKeys.keys = sshKeys;
}; };
users.groups.${mkUsername name} = {}; users.groups.${username} = {};
# Add site group to nginx service # Add site group to nginx service
systemd.services.nginx.serviceConfig.SupplementaryGroups = [ (mkUsername name) ]; systemd.services.nginx.serviceConfig.SupplementaryGroups = [ username ];
# Sudo rules for service management # Sudo rules for service management
security.sudo.extraRules = [{ security.sudo.extraRules = [{
users = [ (mkUsername name) ]; users = [ username ];
commands = [ commands = [
{ {
command = "/run/current-system/sw/bin/systemctl reload phpfpm-${name}"; command = "/run/current-system/sw/bin/systemctl reload phpfpm-${name}";

20
realip.nix Normal file
View file

@ -0,0 +1,20 @@
{ 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;
'';
}

132
static.nix Normal file
View file

@ -0,0 +1,132 @@
{
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 "---"
'';
} ;
}