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

Initial commit

This commit is contained in:
Samuel Štancl 2025-07-23 01:59:06 +02:00
commit 5fab1dceed
9 changed files with 598 additions and 0 deletions

159
README.md Normal file
View file

@ -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 <server_ip> <path_to_your_ssh_key>)`, 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@<your-server-ip>`
- 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 <server_ip> <path_to_your_ssh_key>)` (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@<your server ip>:/etc/nixos/` and start writing config as above.

34
anywhere/auto.sh Executable file
View file

@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -xe
if [ -z "$1" ] || [ -z "$2" ]; then
echo "Usage: $0 <ip-address> <ssh-pubkey-path>"
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

View file

@ -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";
}

56
anywhere/disk-config.nix Normal file
View file

@ -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"
];
};
};
};
};
};
};
}

16
anywhere/flake.nix Normal file
View file

@ -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
];
};
};
}

187
laravel.nix Normal file
View file

@ -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" ];
}
];
}];
}

37
postinstall/auto.sh Executable file
View file

@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -xe
if [ -z "$1" ] || [ -z "$2" ]; then
echo "Usage: $0 <ip-address> <ssh-pubkey-path>"
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

View file

@ -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";
}

17
postinstall/flake.nix Normal file
View file

@ -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
];
};
};
}