From 5fab1dceedcc49debc839c1813dd741bf18ca844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 23 Jul 2025 01:59:06 +0200 Subject: [PATCH] Initial commit --- README.md | 159 +++++++++++++++++++++++++++++ anywhere/auto.sh | 34 +++++++ anywhere/configuration.nix | 34 +++++++ anywhere/disk-config.nix | 56 ++++++++++ anywhere/flake.nix | 16 +++ laravel.nix | 187 ++++++++++++++++++++++++++++++++++ postinstall/auto.sh | 37 +++++++ postinstall/configuration.nix | 58 +++++++++++ postinstall/flake.nix | 17 ++++ 9 files changed, 598 insertions(+) create mode 100644 README.md create mode 100755 anywhere/auto.sh create mode 100644 anywhere/configuration.nix create mode 100644 anywhere/disk-config.nix create mode 100644 anywhere/flake.nix create mode 100644 laravel.nix create mode 100755 postinstall/auto.sh create mode 100644 postinstall/configuration.nix create mode 100644 postinstall/flake.nix diff --git a/README.md b/README.md new file mode 100644 index 0000000..1a9b8f4 --- /dev/null +++ b/README.md @@ -0,0 +1,159 @@ +# Nix scripts + +A collection of scripts and configuration files for our use of Nix tooling. + +## Setting up a new server + +This is just for getting a working NixOS installation with `/etc/nixos/configuration.nix` deployed onto a generic cloud VM. + +The setup also uses `/etc/nixos/flake.nix` since that's an easy way of addressing +[the nixos-anywhere NIX_PATH issue](https://nix-community.github.io/nixos-anywhere/howtos/nix-path.html) +and you likely want to use flakes anyway. + +**Note: All of the automated scripts for the steps below assume you're logging in as root**. If that's not the case, just follow +the steps manually. The scripts will also create lockfiles in `anywhere/` and `postinstall/` to make future deployments consistent +and faster (by reusing more things from your nix store). Feel free to delete those if you want a completely fresh install each time. + +This section is overall just a thin wrapper around nixos-anywhere. + +### Installing NixOS + +- Provision a new server. This config works on Hetzner Cloud, may require adjustments for other + providers, see anywhere/flake.nix + - The default config uses `aarch64`, you can change this to `x86_64` +- Preferably use passwordless auth with just your SSH key + +> Cross-compilation is sometimes buggy so it's recommended to run this on Linux (use a NixOS VM if you're on macOS), preferably +> matching the server's ISA. On macOS I highly recommend creating a NixOS VM (helpful for development anyway) in Parallels with +> no desktop environment, ssh enabled, and shared folders. +> +> That said, running this on macOS *should* still work fine, again ideally on the same ISA as the server (hence the aarch64 default). + +Now either run `(cd anywhere && ./auto.sh )`, with the path being e.g. `~/.ssh/id_ed25519.pub`. Or +if you want to do this manually (or make customizations): +- **Put the key into anywhere/configuration.nix (the REPLACEME) so you can log in after NixOS is installed** +- Run `nix run nixpkgs#nixos-anywhere -- --flake .#cloud root@` + - Replace the output name if you've changed it + - The user doesn't have to be root but has to be able to `sudo` without entering a password + - You need Nix installed with the `nix-command` experimental feature enabled. + If this doesn't work for you on macOS, you can run this from a VM (preferably matching the server ISA). +- If everything goes well, the server will reboot. Shortly after that you should be able to ssh into the server and get root access + - The server will also have a new SSH key, so you'll have to clear old records from `~/.ssh/known_hosts` + +### Adding basic configuration + +**Make sure you've removed the server's previous key from `~/.ssh/known_hosts` if you've connected to the server before!** + +Following successful installation, run `(cd postinstall && ./auto.sh )` (once the server has rebooted). Or if you want to +do this manually: +- ssh into the server and run `nixos-generate-config` +- replace `/etc/nixos/configuration.nix` with `postinstall/configuration.nix` from this repo +- copy `postinstall/flake.nix` to `/etc/nixos/flake.nix` +- `nixos-rebuild switch` + +### Next steps + +Configure your NixOS server as you want. The only things to keep in mind are: +- there are no channels configured +- it's using a flake for the system config and setting the nix path in `/etc/nixos/flake.nix` +- the server's hostname is nixos + +You may want to change the hostname, pull in some flake with system config for that particular hostname, or you +may want to just import some modules into your config. + +## Setting up a Laravel app + +After you have a NixOS server set up, you can use our `laravel.nix` module to start configuring Laravel sites. + +The module is fairly generic so it should work for most sites. It's written in a simple way, to be as easy to +customize as possible if needed, while offering enough customization for most applications. + +Import the module in your system flake and invoke it with these parameters: +```nix +(laravelSite { + name = "mysite"; + domain = "mysite.com"; + phpPackage = pkgs.php84; + + ssl = true; # optional, defaults to false + extraNginxConfig = "nginx configuration string"; # optional + sshKeys = [ "array" "of" "public" "ssh" "keys" ]; # optional + extraPackages = [ pkgs.nodejs_24 ]; # optional + queue = true; # start a queue worker - defaults to false, optional + queueArgs = "--tries=3"; # optional, default empty + generateSshKey = false; # optional, defaults to true + poolSettings = { # optional + "pm.max_children" = 12; + "php_admin_value[opcache_memory_consumption]" = "512"; + "php_admin_flag[opcache.validate_timestamps]" = true; + }; +}) +``` + +The module creates a new user (`laravel-${name}`), a `/srv/${name}` directory, configures +cron to run every minute optionally starts a queue worker and configures php-fpm with +good defaults (see below). The user has a home directory in `/home/laravel-${name}` +(used mainly for `./cache` used by composer and npm) and the site is served from the srv +directory. + +The default php-fpm opcache configuration is to cache everything *forever* without any +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). + +Also, if you're using `ssl` you should put this line into your system config: +```nix +security.acme.email = "your@email.com"; +``` + +A full system config can look something like this (excluding any additional configuration +you may want to make): +```nix +{ + description = "System flake"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + }; + + outputs = { self, nixpkgs, ... }@inputs: { + nixosConfigurations = let + system = "aarch64-linux"; + pkgs = nixpkgs.legacyPackages.${system}; + laravelSite = import ./laravel.nix; + in { + nixos = nixpkgs.lib.nixosSystem { + inherit system; + + modules = [ + { nix.nixPath = [ "nixpkgs=${inputs.nixpkgs}" ]; } + ./configuration.nix + + # your (laravelSite { ... }) calls here + ]; + }; + }; + }; +} +``` + +There's a million different ways to structure your system flake, so you may prefer to use +something different. Note that `laravel.nix` is explicitly not a flake and not a top-level +"input" - the goal is to just invoke it each time *to change system configuration*. We don't +want an additional lockfile for the laravel module and we don't want to update the system +lockfile whenever we make changes to the laravel module. With the most basic configuration, +you should only have `nixpkgs` in your lockfile. + +There also isn't any special shell since Laravel is entirely handled by system daemons like +nginx, php-fpm, cron, and optionally a queue worker systemd service. We do include a .bashrc +with some echos to quickly remind you of the filesystem structure and available commands. + +Simply `scp laravel.nix root@:/etc/nixos/` and start writing config as above. diff --git a/anywhere/auto.sh b/anywhere/auto.sh new file mode 100755 index 0000000..bd1dae3 --- /dev/null +++ b/anywhere/auto.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +set -xe + +if [ -z "$1" ] || [ -z "$2" ]; then + echo "Usage: $0 " + exit 1 +fi + +IP=$1 +SSHKEYPATH=$2 + +TMPDIR=$(mktemp -d) + +cleanup() { + rm -rf "$TMPDIR" +} + +trap cleanup EXIT + +cp configuration.nix "$TMPDIR/configuration.nix" +cp flake.nix "$TMPDIR/flake.nix" +if [ -f flake.lock ]; then + cp flake.lock "$TMPDIR/flake.lock" +fi +cp disk-config.nix "$TMPDIR/disk-config.nix" +sed -i.bak "s|# REPLACEME|\"$(cat "$SSHKEYPATH" | tr -d '\n')\"|" "$TMPDIR/configuration.nix" + +(cd "$TMPDIR" && nix run nixpkgs#nixos-anywhere -- --flake .#cloud root@$IP) + +# Copy the lockfile back. +# This will create a dirty git state but the lock file may be desirable when +# deploying to multiple servers to keep things in sync and reuse more cache. +cp "$TMPDIR/flake.lock" flake.lock diff --git a/anywhere/configuration.nix b/anywhere/configuration.nix new file mode 100644 index 0000000..4c2bd29 --- /dev/null +++ b/anywhere/configuration.nix @@ -0,0 +1,34 @@ +# This config only configures the server, it will not be placed in /etc/nixos +# It should include everything needed to: +# - connect to the server +# - configure the server further + +{ modulesPath, lib, pkgs, ... }: { + imports = [ + (modulesPath + "/installer/scan/not-detected.nix") + (modulesPath + "/profiles/qemu-guest.nix") + ./disk-config.nix + ]; + + boot.loader.grub = { + # no need to set devices, disko will add all devices that have a EF02 partition to the list already + # devices = [ ]; + efiSupport = true; + efiInstallAsRemovable = true; + }; + + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + + environment.systemPackages = map lib.lowPrio [ + pkgs.vim + pkgs.curl + pkgs.git + ]; + + services.openssh.enable = true; + users.users.root.openssh.authorizedKeys.keys = [ + # REPLACEME + ]; + + system.stateVersion = "25.05"; +} diff --git a/anywhere/disk-config.nix b/anywhere/disk-config.nix new file mode 100644 index 0000000..4d7faf0 --- /dev/null +++ b/anywhere/disk-config.nix @@ -0,0 +1,56 @@ +# Example to create a bios compatible gpt partition +# Taken from https://github.com/nix-community/nixos-anywhere-examples/blob/main/disk-config.nix +{ lib, ... }: { + disko.devices = { + disk.disk1 = { + device = lib.mkDefault "/dev/sda"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + boot = { + name = "boot"; + size = "1M"; + type = "EF02"; + }; + esp = { + name = "ESP"; + size = "500M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + }; + }; + root = { + name = "root"; + size = "100%"; + content = { + type = "lvm_pv"; + vg = "pool"; + }; + }; + }; + }; + }; + lvm_vg = { + pool = { + type = "lvm_vg"; + lvs = { + root = { + size = "100%FREE"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + mountOptions = [ + "defaults" + ]; + }; + }; + }; + }; + }; + }; +} diff --git a/anywhere/flake.nix b/anywhere/flake.nix new file mode 100644 index 0000000..bcfcaf7 --- /dev/null +++ b/anywhere/flake.nix @@ -0,0 +1,16 @@ +{ + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + inputs.disko.url = "github:nix-community/disko"; + inputs.disko.inputs.nixpkgs.follows = "nixpkgs"; + + outputs = { nixpkgs, disko, ... }: { + # See other examples at https://github.com/nix-community/nixos-anywhere-examples/blob/main/flake.nix + nixosConfigurations.cloud = nixpkgs.lib.nixosSystem { + system = "aarch64-linux"; + modules = [ + disko.nixosModules.disko + ./configuration.nix + ]; + }; + }; +} diff --git a/laravel.nix b/laravel.nix new file mode 100644 index 0000000..2cbbb91 --- /dev/null +++ b/laravel.nix @@ -0,0 +1,187 @@ +{ name, domain, ssl ? false, extraNginxConfig ? null, sshKeys ? null, phpPackage, extraPackages ? [], queue ? false, queueArgs ? "", generateSshKey ? true, poolSettings ? { + "pm" = "dynamic"; + "pm.max_children" = 8; + "pm.start_servers" = 2; + "pm.min_spare_servers" = 1; + "pm.max_spare_servers" = 3; + "pm.max_requests" = 200; + + "php_admin_flag[opcache.enable]" = true; + "php_admin_value[opcache.memory_consumption]" = "256"; + "php_admin_value[opcache.max_accelerated_files]" = "10000"; + "php_admin_value[opcache.revalidate_freq]" = "0"; + "php_admin_flag[opcache.validate_timestamps]" = false; + "php_admin_flag[opcache.save_comments]" = true; +}, ... }: + +{ config, lib, pkgs, ... }: +let + mkUsername = siteName: "laravel-${siteName}"; +in { + # Ensure nginx is enabled + services.nginx.enable = true; + + # Setup ACME if SSL is enabled + security.acme.acceptTerms = lib.mkIf ssl true; + + # Create welcome message for user + environment.etc."laravel-${name}-bashrc".text = '' + # Laravel site welcome message + echo "Welcome to ${name} Laravel site!" + 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"''} + ${lib.optionalString generateSshKey ''echo "SSH public key: cat ~/.ssh/id_ed25519.pub"''} + echo "---" + ''; + + # Ensure directories exist with proper permissions + systemd.tmpfiles.rules = [ + "d /srv 0755 root root - -" + "d /home 0755 root root - -" + "d /srv/${name} 0755 ${mkUsername name} ${mkUsername name} - -" + "C /home/${mkUsername name}/.bashrc 0644 ${mkUsername name} ${mkUsername name} - /etc/laravel-${name}-bashrc" + ]; + + # Laravel cron job for scheduler + services.cron.systemCronJobs = [ + "* * * * * ${mkUsername name} cd /srv/${name} && ${phpPackage}/bin/php artisan schedule:run > /dev/null 2>&1" + ]; + + # Laravel queue worker service + systemd.services."laravel-queue-${name}" = lib.mkIf queue { + description = "Laravel Queue Worker for ${name}"; + after = [ "network.target" "phpfpm-${name}.service" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "simple"; + User = mkUsername name; + Group = mkUsername name; + WorkingDirectory = "/srv/${name}"; + ExecStart = "${phpPackage}/bin/php artisan queue:work ${queueArgs}"; + Restart = "always"; + RestartSec = 10; + KillMode = "mixed"; + KillSignal = "SIGTERM"; + TimeoutStopSec = 60; + }; + }; + + # SSH key generation for git deployments + systemd.services."generate-ssh-key-${name}" = lib.mkIf generateSshKey { + description = "Generate SSH key for ${mkUsername name}"; + wantedBy = [ "multi-user.target" ]; + after = [ "users.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + User = "root"; + }; + script = '' + 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 ${mkUsername name}" + mkdir -p "$SSH_DIR" + ${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f "$KEY_FILE" -N "" -C "${mkUsername name}@$(hostname)" + chown -R ${mkUsername name}:${mkUsername name} "$SSH_DIR" + chmod 700 "$SSH_DIR" + chmod 600 "$KEY_FILE" + chmod 644 "$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 ${mkUsername name}" + fi + ''; + }; + + # Nginx virtual host configuration + services.nginx.virtualHosts.${domain} = { + enableACME = ssl; + forceSSL = ssl; + root = "/srv/${name}/public"; + + extraConfig = '' + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-Content-Type-Options "nosniff"; + charset utf-8; + index index.php; + error_page 404 /index.php; + ${lib.optionalString (extraNginxConfig != null) extraNginxConfig} + ''; + + locations = { + "/" = { + tryFiles = "$uri $uri/ /index.php?$query_string"; + }; + + "= /favicon.ico".extraConfig = '' + access_log off; + log_not_found off; + ''; + + "= /robots.txt".extraConfig = '' + access_log off; + log_not_found off; + ''; + + "~ ^/index\\.php(/|$)".extraConfig = '' + fastcgi_pass unix:${config.services.phpfpm.pools.${name}.socket}; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + include ${pkgs.nginx}/conf/fastcgi_params; + fastcgi_hide_header X-Powered-By; + ''; + + "~ /\\.(?!well-known).*".extraConfig = '' + deny all; + ''; + }; + }; + + # PHP-FPM pool configuration + services.phpfpm.pools.${name} = { + user = mkUsername name; + phpPackage = phpPackage; + settings = poolSettings // { + "listen.owner" = config.services.nginx.user; + }; + }; + + # User and group settings + users.users.${mkUsername name} = { + group = mkUsername name; + isSystemUser = true; + createHome = true; + home = "/home/${mkUsername name}"; + homeMode = "750"; + shell = pkgs.bashInteractive; + packages = [ phpPackage pkgs.git pkgs.unzip phpPackage.packages.composer ] ++ extraPackages; + } // lib.optionalAttrs (sshKeys != null) { + openssh.authorizedKeys.keys = sshKeys; + }; + + users.groups.${mkUsername name} = {}; + + # Add site group to nginx service + systemd.services.nginx.serviceConfig.SupplementaryGroups = [ (mkUsername name) ]; + + # Sudo rule for reloading PHP-FPM + security.sudo.extraRules = [{ + users = [ (mkUsername name) ]; + commands = [ + { + command = "/run/current-system/sw/bin/systemctl reload phpfpm-${name}"; + options = [ "NOPASSWD" ]; + } + { + command = "/run/current-system/sw/bin/systemctl reload phpfpm-${name}.service"; + options = [ "NOPASSWD" ]; + } + ]; + }]; +} diff --git a/postinstall/auto.sh b/postinstall/auto.sh new file mode 100755 index 0000000..150d136 --- /dev/null +++ b/postinstall/auto.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -xe + +if [ -z "$1" ] || [ -z "$2" ]; then + echo "Usage: $0 " + exit 1 +fi + +IP=$1 +SSHKEYPATH=$2 + +TMPDIR=$(mktemp -d) + +cleanup() { + rm -rf "$TMPDIR" +} + +trap cleanup EXIT + +cp configuration.nix "$TMPDIR/configuration.nix" +sed -i.bak "s|# REPLACEME|\"$(cat "$SSHKEYPATH" | tr -d '\n')\"|" "$TMPDIR/configuration.nix" + +echo "$TMPDIR/configuration.nix" + +ssh "root@$IP" "nixos-generate-config" +scp "$TMPDIR/configuration.nix" "root@$IP:/etc/nixos/configuration.nix" +scp flake.nix "root@$IP:/etc/nixos/flake.nix" +if [ -f flake.lock ]; then + scp flake.lock "root@$IP:/etc/nixos/flake.lock" +fi +ssh "root@$IP" "nixos-rebuild switch" + +# Copy the lockfile back. +# This will create a dirty git state but the lock file may be desirable when +# deploying to multiple servers to keep things in sync and reuse more cache. +scp "root@$IP:/etc/nixos/flake.lock" flake.lock diff --git a/postinstall/configuration.nix b/postinstall/configuration.nix new file mode 100644 index 0000000..1e54362 --- /dev/null +++ b/postinstall/configuration.nix @@ -0,0 +1,58 @@ +# Edit this configuration file to define what should be installed on +# your system. Help is available in the configuration.nix(5) man page, on +# https://search.nixos.org/options and in the NixOS manual (`nixos-help`). + +{ config, lib, pkgs, ... }: + +{ + imports = [ + ./hardware-configuration.nix + ]; + + boot.loader.grub = { + efiSupport = true; + device = "nodev"; + efiInstallAsRemovable = true; + }; + + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + + networking.hostName = "nixos"; + networking.networkmanager.enable = true; + + # Set your time zone. + time.timeZone = "UTC"; + + environment.systemPackages = with pkgs; [ + vim + git + curl + ghostty.terminfo + wget + ]; + + # Define a user account. Don't forget to set a password with ‘passwd’. + # users.users.alice = { + # isNormalUser = true; + # extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user. + # packages = with pkgs; [ + # tree + # ]; + # }; + + # Enable the OpenSSH daemon. + services.openssh.enable = true; + users.users.root.openssh.authorizedKeys.keys = [ + # REPLACEME + ]; + + # Open ports in the firewall. + # networking.firewall.allowedTCPPorts = [ ... ]; + # networking.firewall.allowedUDPPorts = [ ... ]; + # Or disable the firewall altogether. + # networking.firewall.enable = false; + + # Never change this + system.stateVersion = "25.05"; +} + diff --git a/postinstall/flake.nix b/postinstall/flake.nix new file mode 100644 index 0000000..58da156 --- /dev/null +++ b/postinstall/flake.nix @@ -0,0 +1,17 @@ +{ + description = "System configuration"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + }; + + outputs = { self, nixpkgs, ... }@inputs: { + nixosConfigurations.nixos = nixpkgs.lib.nixosSystem { + system = "aarch64-linux"; + modules = [ + { nix.nixPath = [ "nixpkgs=${inputs.nixpkgs}" ]; } + ./configuration.nix + ]; + }; + }; +}