1
0
Fork 0
mirror of https://github.com/archtechx/nix.git synced 2025-12-12 03:24:02 +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
your deployment script.
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).
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).
Also, if you're using `ssl` you should put this line into your system config:
```nix
@ -170,15 +171,33 @@ Simply `scp laravel.nix root@<your server ip>:/etc/nixos/` and start writing con
### 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
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@<server ip>:/etc/nixos/`
>
> Then just add `./catchall.nix` to your modules array.
```nix
{
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
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
# 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
```
## 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
@ -323,3 +381,11 @@ 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.

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
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
@ -31,7 +32,7 @@
{ config, lib, pkgs, ... }:
let
mkUsername = siteName: "laravel-${siteName}";
username = "laravel-${name}";
in {
services.nginx.enable = true;
security.acme.acceptTerms = lib.mkIf ssl true;
@ -41,13 +42,14 @@ 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/${mkUsername name}"
echo "User home: /home/${username}"
echo "Site: /srv/${name}"
echo "Restart php-fpm: sudo systemctl reload phpfpm-${name}"
${lib.optionalString queue ''echo "Restart queue: php artisan queue:restart"''}
@ -60,12 +62,12 @@ in {
systemd.tmpfiles.rules = [
"d /srv 0751 root root - -"
"d /home 0751 root root - -"
"d /srv/${name} 0750 ${mkUsername name} ${mkUsername name} - -"
"C /home/${mkUsername name}/.bashrc 0640 ${mkUsername name} ${mkUsername name} - /etc/laravel-${name}-bashrc"
"d /srv/${name} 0750 ${username} ${username} - -"
"C /home/${username}/.bashrc 0640 ${username} ${username} - /etc/laravel-${name}-bashrc"
];
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
@ -75,8 +77,8 @@ in {
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
User = mkUsername name;
Group = mkUsername name;
User = username;
Group = username;
WorkingDirectory = "/srv/${name}";
ExecStart = "${phpPackage}/bin/php artisan queue:work ${queueArgs}";
Restart = "always";
@ -88,8 +90,9 @@ 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 ${mkUsername name}";
description = "Generate SSH key for ${username}";
wantedBy = [ "multi-user.target" ];
after = [ "users.target" ];
serviceConfig = {
@ -98,15 +101,15 @@ in {
User = "root";
};
script = ''
USER_HOME="/home/${mkUsername name}"
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 ${mkUsername name}"
echo "Generating SSH key for ${username}"
mkdir -p "$SSH_DIR"
${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f "$KEY_FILE" -N "" -C "${mkUsername name}"
chown -R ${mkUsername name}:${mkUsername name} "$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"
@ -114,7 +117,7 @@ in {
echo "Public key for deploy key:"
cat "$KEY_FILE.pub"
else
echo "SSH key already exists for ${mkUsername name}"
echo "SSH key already exists for ${username}"
fi
'';
};
@ -169,11 +172,18 @@ 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 = mkUsername name;
user = username;
phpPackage = phpPackage;
settings = poolSettings // extraPoolSettings // {
"listen.owner" = config.services.nginx.user;
@ -181,11 +191,11 @@ in {
};
# User and group settings
users.users.${mkUsername name} = {
group = mkUsername name;
users.users.${username} = {
group = username;
isSystemUser = true;
createHome = true;
home = "/home/${mkUsername name}";
home = "/home/${username}";
homeMode = "750";
shell = pkgs.bashInteractive;
packages = [ phpPackage pkgs.git pkgs.unzip phpPackage.packages.composer ] ++ extraPackages;
@ -193,14 +203,14 @@ in {
openssh.authorizedKeys.keys = sshKeys;
};
users.groups.${mkUsername name} = {};
users.groups.${username} = {};
# Add site group to nginx service
systemd.services.nginx.serviceConfig.SupplementaryGroups = [ (mkUsername name) ];
systemd.services.nginx.serviceConfig.SupplementaryGroups = [ username ];
# Sudo rules for service management
security.sudo.extraRules = [{
users = [ (mkUsername name) ];
users = [ username ];
commands = [
{
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 "---"
'';
} ;
}