diff options
| author | user@node5.net <user@node5.net> | 2026-05-30 00:17:23 +0200 |
|---|---|---|
| committer | user@node5.net <user@node5.net> | 2026-05-30 00:17:23 +0200 |
| commit | 0802a3305be33be03dc4615d7b85e76eb5c672ea (patch) | |
| tree | e01d9e7b2e09dd6d813d9ca7446b50c02716e741 | |
| parent | a9b9d3e22774e7ea19e8b70d8a1b0b81a0928ad1 (diff) | |
new article - Nix on my server
deployment, front-page, blog done
| -rw-r--r-- | NixOS on my server/Thumbnail.webp | bin | 0 -> 44770 bytes | |||
| -rw-r--r-- | NixOS on my server/index.md | 653 |
2 files changed, 653 insertions, 0 deletions
diff --git a/NixOS on my server/Thumbnail.webp b/NixOS on my server/Thumbnail.webp new file mode 100644 index 0000000..0007955 --- /dev/null +++ b/NixOS on my server/Thumbnail.webp Binary files differdiff --git a/NixOS on my server/index.md b/NixOS on my server/index.md new file mode 100644 index 0000000..f3b9f10 --- /dev/null +++ b/NixOS on my server/index.md @@ -0,0 +1,653 @@ +--- +description: Updating over wireguard without cutting the branch you're sitting on, migrating services +created: 2026-05-09 +--- + +I wish to try out nix on server infrastructure, my public server is the least critical server, +as it mainly serves as my playground. +I will be deploying nix the nix way, to get the full benefits. +This means transition all my services to being fully declared with nix. + +My services: +- 1 static NGINX website +- 1 CGit instance +- 4 python flask applications +- 1 postgres database with postgis +- 1 tor hidden service + +and a bunch of regular server setup built up over the years, +you'll be supprised how many small things you've set up over the years + + +## Deployment with automatic rollback if unreachable + +### Problem + +I recently set out to migrate this server to NixOS. +It's only available over wireguard, when running `nixos-rebuild switch` with the wireguard address as `--target-host`, +it's really easy to set some config option that makes the system unreachable. + +### Simple native solution + +Recovering is easy, there's a command to switch back to the booted system. + +```console +[root@node5:~]# ls -lah /run/*system +lrwxrwxrwx 1 root root 85 May 2 13:31 /run/booted-system -> /nix/store/ksj77alpblymmnhfyzb3r5vlb4d7qhr8-nixos-system-node5-25.11.20260415.1766437 +lrwxrwxrwx 1 root root 85 May 2 13:31 /run/current-system -> /nix/store/ksj77alpblymmnhfyzb3r5vlb4d7qhr8-nixos-system-node5-25.11.20260415.1766437 +``` + +```console +[root@node5:~]# /run/booted-system/bin/switch-to-configuration +Usage: switch-to-configuration [check|switch|boot|test|dry-activate] +check: run pre-switch checks and exit +switch: make the configuration the boot default and activate now +boot: make the configuration the boot default +test: activate the configuration, but don't make it the boot default +dry-activate: show what would be done if this configuration were activated + +``` + +Now it would be nice if there was an automated rollback in case the system became unreachable. +This could be as simple as: run a root tmux with `sleep 300 && /run/booted-system/bin/switch-to-configuration` +However what does it do if an activation take more than 5 minutes, what if you forget? +Plus i even had once where the wireguard service didn't come up by it self again. +It would be nicer with a purpose build tool. + +### deploy-rs + +[deploy-rs - github.com](https://github.com/serokell/deploy-rs) seems to fit the bill with it's ✨Magic Rollback✨ + +*"There is a built-in feature to prevent you making changes that might render your machine unconnectable or unusuable, +which works by connecting to the machine after profile activation to confirm the machine is still available, +and instructing the target node to automatically roll back if it is not confirmed"* +- [deploy-rs readme](https://github.com/serokell/deploy-rs/blob/77c906c0ba56aabdbc72041bf9111b565cdd6171/README.md#:~:text=Rollback-,There,confirmed%2E) + +Here's a nice [deploy-rs setup guide - crystalwobsite.gay](https://crystalwobsite.gay/posts/2025-02-09-deploying_nixos#deploying-via-flakes) + +<pre> +<span style="font-weight:bold;">diff --git a/flake.nix b/flake.nix</span> +<span style="font-weight:bold;">index a056d72..b47d632 100644</span> +<span style="font-weight:bold;">--- a/flake.nix</span> +<span style="font-weight:bold;">+++ b/flake.nix</span> +<span style="color:teal;">@@ -23,9 +23,11 @@</span> + }; +␠ + node5-nvim.url = "git+https://git.node5.net/nix/nvim"; +<span style="color:lime;">+</span> +<span style="color:lime;">+</span><span style="color:lime;"> deploy-rs.url = "github:serokell/deploy-rs";</span> + }; +␠ +<span style="color:red;">- outputs = { self, nixpkgs, nixpkgs-unstable, home-manager, ... } @ inputs:</span> +<span style="color:lime;">+</span><span style="color:lime;"> outputs = { self, nixpkgs, nixpkgs-unstable, home-manager, deploy-rs, ... } @ inputs:</span> + let + inherit (self) outputs; + system = "x86_64-linux"; +<span style="color:teal;">@@ -73,6 +75,32 @@</span> + ]; + }; +␠ +<span style="color:lime;">+</span><span style="color:lime;"> node5-test = nixpkgs.lib.nixosSystem {</span> +<span style="color:lime;">+</span><span style="color:lime;"> specialArgs = {inherit inputs unstable; };</span> +<span style="color:lime;">+</span><span style="color:lime;"> modules = [</span> +<span style="color:lime;">+</span><span style="color:lime;"> ./modules/hosts/node5-test/configuration.nix</span> +<span style="color:lime;">+</span><span style="color:lime;"> ./modules/common.nix # nixos stuff I want on all machines</span> +<span style="color:lime;">+</span><span style="color:lime;"> ];</span> +<span style="color:lime;">+</span><span style="color:lime;"> };</span> +<span style="color:lime;">+</span> + }; +<span style="color:lime;">+</span> +<span style="color:lime;">+</span><span style="color:lime;"> deploy = {</span> +<span style="color:lime;">+</span><span style="color:lime;"> nodes = {</span> +<span style="color:lime;">+</span><span style="color:lime;"> node5-test = {</span> +<span style="color:lime;">+</span><span style="color:lime;"> hostname = "192.168.1.63";</span> +<span style="color:lime;">+</span><span style="color:lime;"> sshUser = "root";</span> +<span style="color:lime;">+</span><span style="color:lime;"> profiles = {</span> +<span style="color:lime;">+</span><span style="color:lime;"> system = {</span> +<span style="color:lime;">+</span><span style="color:lime;"> user = "root";</span> +<span style="color:lime;">+</span><span style="color:lime;"> path =</span> +<span style="color:lime;">+</span><span style="color:lime;"> deploy-rs.lib.${system}.activate.nixos</span> +<span style="color:lime;">+</span><span style="color:lime;"> self.nixosConfigurations.node5-test;</span> +<span style="color:lime;">+</span><span style="color:lime;"> };</span> +<span style="color:lime;">+</span><span style="color:lime;"> };</span> +<span style="color:lime;">+</span><span style="color:lime;"> };</span> +<span style="color:lime;">+</span><span style="color:lime;"> };</span> +<span style="color:lime;">+</span><span style="color:lime;"> };</span> +<span style="color:lime;">+</span> + }; + } +</pre> + +<pre> +❯ deploy . + +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Running checks for flake in . +<span style="font-weight:bold;color:purple;">warning:</span> Git tree '/home/user/dot-files' is dirty +<span style="font-weight:bold;color:purple;">warning:</span> unknown flake output 'deploy' +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Evaluating flake in . +<span style="font-weight:bold;color:purple;">warning:</span> Git tree '/home/user/dot-files' is dirty +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] The following profiles are going to be deployed: +[node5-test.system] +user = "root" +ssh_user = "root" +path = "/nix/store/z8c3vc2689lbcvplhs42iqzbbb7x7k9s-activatable-nixos-system-node5-25.11.20260415.1766437" +hostname = "192.168.1.63" +ssh_opts = [] + +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Building profile `system` for node `node5-test` +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Copying profile `system` to node `node5-test` +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Activating profile `system` for node `node5-test` +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Creating activation waiter +👀 ℹ️ [wait] [<span style="color:#00ffff;">INFO</span>] Waiting for confirmation event... +⭐ ℹ️ [activate] [<span style="color:#00ffff;">INFO</span>] Activating profile +activating the configuration... +setting up /etc... +reloading user units for user... +reloading user units for root... +restarting sysinit-reactivation.target +the following new units were started: NetworkManager-dispatcher.service +⭐ ℹ️ [activate] [<span style="color:#00ffff;">INFO</span>] Activation succeeded! +⭐ ℹ️ [activate] [<span style="color:#00ffff;">INFO</span>] Magic rollback is enabled, setting up confirmation hook... +👀 ℹ️ [wait] [<span style="color:#00ffff;">INFO</span>] Found canary file, done waiting! +⭐ ℹ️ [activate] [<span style="color:#00ffff;">INFO</span>] Waiting for confirmation event... +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Success activating, attempting to confirm activation +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Deployment confirmed. + + ~/dot-files master *4 +6 !5 ──────────────────────────────────── 52s impure 21:36:55 +</pre> + +It deploys a working config successfully, +now let's change the config such that we no longer have SSH access to the server + +<pre> +<span style="color:red;">- networking.firewall.allowedTCPPorts = [ 22 ];</span> +</pre> + +<pre> +❯ deploy . +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Running checks for flake in . +<span style="font-weight:bold;color:purple;">warning:</span> Git tree '/home/user/dot-files' is dirty +<span style="font-weight:bold;color:purple;">warning:</span> unknown flake output 'deploy' +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Evaluating flake in . +<span style="font-weight:bold;color:purple;">warning:</span> Git tree '/home/user/dot-files' is dirty +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] The following profiles are going to be deployed: +[node5-test.system] +user = "root" +ssh_user = "root" +path = "/nix/store/2dfsx5blqqib25ir0v32azqn2g49d267-activatable-nixos-system-node5-25.11.20260415.1766437" +hostname = "192.168.1.63" +ssh_opts = [] + +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Building profile `system` for node `node5-test` +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Copying profile `system` to node `node5-test` +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Activating profile `system` for node `node5-test` +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Creating activation waiter +👀 ℹ️ [wait] [<span style="color:#00ffff;">INFO</span>] Waiting for confirmation event... +⭐ ℹ️ [activate] [<span style="color:#00ffff;">INFO</span>] Activating profile +activating the configuration... +setting up /etc... +reloading user units for user... +reloading user units for root... +restarting sysinit-reactivation.target +reloading the following units: nftables.service +the following new units were started: NetworkManager-dispatcher.service +⭐ ℹ️ [activate] [<span style="color:#00ffff;">INFO</span>] Activation succeeded! +⭐ ℹ️ [activate] [<span style="color:#00ffff;">INFO</span>] Magic rollback is enabled, setting up confirmation hook... +👀 ℹ️ [wait] [<span style="color:#00ffff;">INFO</span>] Found canary file, done waiting! +⭐ ℹ️ [activate] [<span style="color:#00ffff;">INFO</span>] Waiting for confirmation event... +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Success activating, attempting to confirm activation +⭐ ⚠️ [activate] [<span style="color:#ff8700;">WARN</span>] De-activating due to error +switching profile from version 21 to 20 +⭐ ⚠️ [activate] [<span style="color:#ff8700;">WARN</span>] Removing generation by ID 21 +removing profile version 21 +⭐ ℹ️ [activate] [<span style="color:#00ffff;">INFO</span>] Attempting to re-activate the last generation +activating the configuration... +setting up /etc... +reloading user units for user... +reloading user units for root... +restarting sysinit-reactivation.target +reloading the following units: nftables.service +the following new units were started: NetworkManager-dispatcher.service +⭐ ❌ [activate] [<span style="color:#ff0000;">ERROR</span>] Failed to get activation confirmation: Error waiting for confirmation event: Timeout elapsed for confirmation + +thread 'tokio-runtime-worker' panicked at /build/source/src/deploy.rs:490:41: +called `Result::unwrap()` on an `Err` value: SSHActivateExit(Some(1)) +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Deployment confirmed. +🚀 ❌ [deploy] [<span style="color:#ff0000;">ERROR</span>] Activating over SSH resulted in a bad exit code: RecvError(()) +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Revoking previous deploys +🚀 ❌ [deploy] [<span style="color:#ff0000;">ERROR</span>] Deployment to node node5-test failed, rolled back to previous generation + + ~/dot-files master *4 +6 !5 ───────────────────────────────────────────── ✔ 1|0 1m 29s impure 21:38:27 +</pre> + +--- + +<pre> +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Running checks for flake in /home/user/dot-files/ +<span style="font-weight:bold;color:purple;">warning:</span> Git tree '/home/user/dot-files' is dirty +<span style="font-weight:bold;color:purple;">warning:</span> unknown flake output 'deploy' +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Evaluating flake in /home/user/dot-files/ +<span style="font-weight:bold;color:purple;">warning:</span> Git tree '/home/user/dot-files' is dirty +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] The following profiles are going to be deployed: +[node5-test.system] +user = "root" +ssh_user = "root" +path = "/nix/store/1sqnzii8yiv42v2ci4m2cnx34qc6mima-activatable-nixos-system-node5-test-25.11.20260415.1766437" +hostname = "10.10.41.1" +ssh_opts = [] + +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Building profile `system` for node `node5-test` +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Copying profile `system` to node `node5-test` +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Activating profile `system` for node `node5-test` +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Creating activation waiter +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Success activating, attempting to confirm activation +⭐ ℹ️ [activate] [<span style="color:#00ffff;">INFO</span>] Activating profile +🚀 ℹ️ [deploy] [<span style="color:#00ffff;">INFO</span>] Deployment confirmed. +stopping the following units: wg-quick-wg0.service + +</pre> + + +<pre> +<span style="font-weight:bold;">diff --git a/modules/hosts/node5-test/wireguard.nix b/modules/hosts/node5-test/wireguard.nix</span> +<span style="font-weight:bold;">index 288f7ae..ad86695 100644</span> +<span style="font-weight:bold;">--- a/modules/hosts/node5-test/wireguard.nix</span> +<span style="font-weight:bold;">+++ b/modules/hosts/node5-test/wireguard.nix</span> +<span style="color:teal;">@@ -7,18 +7,24 @@</span> in + allowedUDPPorts = [ listenPort ]; + interfaces."wg0".allowedTCPPorts = [ 22 ]; # SSH from personal systems + }; +<span style="color:red;">- networking.wg-quick.interfaces = {</span> +<span style="color:lime;">+</span><span style="color:lime;"> networking.wireguard.interfaces = {</span> + wg0 = { +<span style="color:red;">- address = [ "10.10.41.1/24" ];</span> +<span style="color:lime;">+</span><span style="color:lime;"> ips = [ "10.10.41.1/24" ];</span> + privateKeyFile = "/etc/secrets/wireguard/privatekey"; + listenPort = listenPort; + peers = [ + { +<span style="color:red;">- # T480s</span> +<span style="color:lime;">+</span><span style="color:lime;"> name = "T480s";</span> + publicKey = "YYjWG9lD4zkjNkjMYH4CfIac1sqsWZknWFh6d4OxmnM="; + presharedKeyFile = "/etc/secrets/wireguard/t480s_presharedkey"; +<span style="color:red;">- allowedIPs = [ "10.10.41.110/24" ];</span> +<span style="color:lime;">+</span><span style="color:lime;"> allowedIPs = [ "10.10.41.110/32" ];</span> + } + ]; + }; + }; +</pre> + +> Note: Allowed IP must be `/32`, `/24` will cause it to silently fail + +--- + +## Front page + +Nginx serving static files + +### Derivations + +Derivations is the way to copy things to the nix store, it's done with the command `stdenv.mkDerivation`, which consists + +1. **Unpack**: handles the preparation of the build environment (e.g. extracting archives, touching files, etc.); +2. **Patch**: handles changes to the underlying source code (e.g. patching bugs, adapting the source code to work in a Nix environment, etc.); +3. **Configure**: handles the configuration of the build environment, for example detecting system capabilities and setting build parameters; +4. **Build**: compiles the source code into binaries, bytecode, or otherwise a distributable form of the source code; +5. **Check**: performs any tests on the compiled package, for example the package's test suite; +6. **Install**: copies the build artefacts to the output directory, handling any needed changes (e.g. directory structure reorganisation); +7. **Fixup**: process the output artefacts to work in a Nix environment (e.g.: strip binaries, override ELF paths, handle dynamic library linking, etc.); +8. **Install Check**: performs any tests on the final output, essentially acting as a integration test into the Nix environment; +9. **Dist**: creates distribution archives (rarely used). + +Read more: [source - wiki.nixos.org](https://wiki.nixos.org/wiki/Derivations#:~:text=By%20common%20convention%2C%20standard%20environment%20derivations%20contain%20the%20following%20phases%2C%20executed%20in%20this%20sequence%3A) + +```nix +{ pkgs, lib, ... }: +let + node5Static = pkgs.stdenv.mkDerivation { + name = "node5-static-site"; + src = ./files; + postInstall = '' + mkdir $out + cp -av ./* $out/ + ''; + }; +in +{ + + networking.firewall.allowedTCPPorts = [ 80 443 ]; + services.nginx.enable = true; + services.nginx.virtualHosts."node5.net" = { + forceSSL = true; + enableACME = true; + root = "${node5Static}"; + }; + security.acme = { + acceptTerms = true; + defaults.email = "lets.encrypt@node5.net"; + }; +} +``` + +--- + +## Blog + +### Packaging as binary + +Following this example +<https://wiki.nixos.org/wiki/Python#With_pyproject.toml> +and adding a bit of meta data, i can now build the application as a command :) + +This means you can run it with `nix run git+https://git.node5.net/blog/blog.node5.net_flask`, +or add it to `environment.systemPackages` with + +`flake.nix` + +```nix +node5-blog.url = "git+https://git.node5.net/blog/blog.node5.net_flask"; +``` + +`configuration.nix` + +```nix +environment.systemPackages = with pkgs; [ + inputs.node5-blog.packages.x86_64-linux.default +] +``` + +```nix +{ + description = "A basic flake using pyproject.toml project metadata"; + + inputs = { + pyproject-nix = { + url = "github:nix-community/pyproject.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { nixpkgs, pyproject-nix, ... }: + let + inherit (nixpkgs) lib; + + project = pyproject-nix.lib.project.loadPyproject { + # Read & unmarshal pyproject.toml relative to this project root. + # projectRoot is also used to set `src` for renderers such as buildPythonPackage. + projectRoot = ./.; + }; + + # This example is only using x86_64-linux + pkgs = nixpkgs.legacyPackages.x86_64-linux; + + python = pkgs.python3; + + in + { + # Build our package using `buildPythonPackage + packages.x86_64-linux.default = + let + # Returns an attribute set that can be passed to `buildPythonPackage`. + attrs = project.renderers.buildPythonPackage { inherit python; }; + in + # Pass attributes to buildPythonPackage. + # Here is a good spot to add on any missing or custom attributes. + python.pkgs.buildPythonPackage (attrs // { + meta = { + description = "Blog backend for blog.node5.net"; + homepage = "https://blog.node5.net/Blog%20meta/"; + changelog = "https://git.node5.net/blog/blog.node5.net_flask/log/"; + mainProgram = "blog-node5"; + }; + }); + }; +} +``` + +<pre> +<span style="font-weight:bold;color:#89DDFF;">result</span> +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">├── </span><span style="font-weight:bold;color:#89DDFF;">bin</span> +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">│ └── </span><span style="font-weight:bold;color:#C3E88D;">blog-node5</span> +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">├── </span><span style="font-weight:bold;color:#89DDFF;">lib</span> +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">│ └── </span><span style="font-weight:bold;color:#89DDFF;">python3.13</span> +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">│ └── </span><span style="font-weight:bold;color:#89DDFF;">site-packages</span> +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">│ ├── </span><span style="font-weight:bold;color:#89DDFF;">__pycache__</span> +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">│ │ ├── </span><span style="color:#C3E88D;">article.cpython-313.opt-1.pyc</span> +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">│ │ ├── </span><span style="color:#C3E88D;">article.cpython-313.pyc</span> +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">│ │ ├── </span><span style="color:#C3E88D;">blog_node5_net.cpython-313.opt-1.pyc</span> +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">│ │ ├── </span><span style="color:#C3E88D;">blog_node5_net.cpython-313.pyc</span> +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">│ │ ├── </span><span style="color:#C3E88D;">db_handler.cpython-313.opt-1.pyc</span> +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">│ │ ├── </span><span style="color:#C3E88D;">db_handler.cpython-313.pyc</span> +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">│ │ ├── </span><span style="color:#C3E88D;">telegram_handler.cpython-313.opt-1.pyc</span> +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">│ │ └── </span><span style="color:#C3E88D;">telegram_handler.cpython-313.pyc</span> +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">│ ├── </span><span style="font-weight:bold;color:#89DDFF;">blog_node5_net-0.1.0.dist-info</span> +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">│ │ ├── </span>entry_points.txt +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">│ │ ├── </span>METADATA +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">│ │ ├── </span>RECORD +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">│ │ ├── </span>top_level.txt +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">│ │ └── </span>WHEEL +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">│ ├── </span><span style="font-weight:bold;color:#C3E88D;">article.py</span> +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">│ ├── </span><span style="font-weight:bold;color:#C3E88D;">blog_node5_net.py</span> +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">│ ├── </span><span style="font-weight:bold;color:#C3E88D;">db_handler.py</span> +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">│ └── </span><span style="font-weight:bold;color:#C3E88D;">telegram_handler.py</span> +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">└── </span><span style="font-weight:bold;color:#89DDFF;">nix-support</span> +<span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;"> └── </span>propagated-build-inputs +</pre> + + +`cat result/nix-support/propagated-build-inputs` + +``` +/nix/store/10hk7srr12wgp2hqm5lai0xxr69m76b7-python3.13-flask-3.1.2 /nix/store/jl0mxihyizv77l66mzbvmv49iiri72sd-python3.13-pyyaml-6.0.3 /nix/store/pkj9yz58kijfwyg4c0xpwc2dlwwswr6s-python3.13-markdown-3.10.2 /nix/store/6svr8x0lzmsn8d70asdc5qns35273216-python3.13-python-telegram-bot-22.7 /nix/store/6snki2zk3rmh13wwi07g3x79a1rr032m-python3.13-pygments-2.20.0 /nix/store/rfhv4bxzg6aqv7ll7d2g3fx7vdj63ks3-python3.13-tabulate-0.10.0 /nix/store/0r6k8xa2kgqyp3r4v2w7yrb80ma2iawm-python3-3.13.12 +``` + +or listed out + +- /nix/store/10hk7srr12wgp2hqm5lai0xxr69m76b7-python3.13-flask-3.1.2 +- /nix/store/jl0mxihyizv77l66mzbvmv49iiri72sd-python3.13-pyyaml-6.0.3 +- /nix/store/pkj9yz58kijfwyg4c0xpwc2dlwwswr6s-python3.13-markdown-3.10.2 +- /nix/store/6svr8x0lzmsn8d70asdc5qns35273216-python3.13-python-telegram-bot-22.7 +- /nix/store/6snki2zk3rmh13wwi07g3x79a1rr032m-python3.13-pygments-2.20.0 +- /nix/store/rfhv4bxzg6aqv7ll7d2g3fx7vdj63ks3-python3.13-tabulate-0.10.0 +- /nix/store/0r6k8xa2kgqyp3r4v2w7yrb80ma2iawm-python3-3.13.12 + +### Prod UWSGI + +Following [the "Official NixOS Wiki" page for UWSGI](https://wiki.nixos.org/wiki/UWSGI) +gives us an example of how to host an application with UWSGI. +It hinges on `pythonPath` to function, this is a list of paths to python packages. + +``` +nix-repl> inputs.nixpkgs.legacyPackages.aarch64-linux.oncall.pythonPath +"/nix/store/w9v0xf1jg5agcxrn8fzl3nxqsrrbxam4-python3-3.13.12/lib/python3.13/site-packages:/nix/store/vwql81c83bdidrnfbf91477i3izhj469-python3.13-beaker-1.13.0/lib/python3.13/site-packages:/nix/store/h7q4mj3p6cc1zdpg0hf2rlf5x7bqjfnx-python3.13-falcon-4.0.2/lib/python3.13/site-packages:/nix/store/wvhh1s7fdkslx02jplcwfyrqhhzp6s84-python3.13-falcon-cors-1.1.7/lib/python3.13/site-packages:/nix/store/9rnq594bh5wzqw1k6npykgxmhgkyvbrv-python3.13-gevent-25.5.1/lib/python3.13/site-packages:/nix/store/hz7x8s6mxmbnlbkxf5h4717hr8ing3g6-python3.13-gunicorn-23.0.0/lib/python3.13/site-packages:/nix/store/jb72aqjm73c45j1zch7647rky6wqmfbf-python3.13-icalendar-6.3.2/lib/python3.13/site-packages:/nix/store/3hqasdzwya752k4lvclmkvzqymj93yqd-python3.13-irisclient-1.2.0/lib/python3.13/site-packages:/nix/store/wrrd7848134g5fxml6rhyy2gy1pszm80-python3.13-jinja2-3.1.6/lib/python3.13/site-packages:/nix/store/2r3gb5z2h6vg4i7jppwxyvwjfj9m7aa9-python3.13-phonenumbers-9.0.10/lib/python3.13/site-packages:/nix/store/v5lryy9ip42l4j8nqb0ai85gs0ps2h8v-python3.13-pymysql-1.1.1/lib/python3.13/site-packages:/nix/store/8llwrni08jgbai4h1gzid2j951zm008d-python3.13-python-ldap-3.4.5/lib/python3.13/site-packages:/nix/store/bkkhkgp46vvxp3cdcr0kkhga0wipqr7g-python3.13-pytz-2025.2/lib/python3.13/site-packages:/nix/store/rvq6x8wh9xrf26r0ar60zmc44g7akhrq-python3.13-pyyaml-6.0.3/lib/python3.13/site-packages:/nix/store/xfq8fkgpm8a5v6sqacs0s0h776547df5-python3.13-ujson-5.10.0/lib/python3.13/site-packages:/nix/store/9bvzi4y2xqk6v6zsqjipx3il1n5q7wyq-python3.13-webassets-2.0/lib/python3.13/site-packages:/nix/store/c0r4ikngyy6b6d11vgg69dp23amn84r6-python3.13-sqlalchemy-2.0.44/lib/python3.13/site-packages:/nix/store/r09fvwifl9hbk07z0v8w28lwj08fq49w-python3.13-pycrypto-3.23.0/lib/python3.13/site-packages:/nix/store/6w5ykz0ql7iq3kl7z0bzj91mfzg2zv88-python3.13-cryptography-46.0.7/lib/python3.13/site-packages:/nix/store/jxnid1fl09j140mb7galy581p0yl8n32-python3.13-greenlet-3.2.3/lib/python3.13/site-packages:/nix/store/zq2wjvr0ggry3l1c6i429yn6mlj06w3g-python3.13-typing-extensions-4.15.0/lib/python3.13/site-packages:/nix/store/sbn0djkjz9y04pi0kqdrr8gr2ch2yqpf-python3.13-pycryptodome-3.23.0/lib/python3.13/site-packages:/nix/store/qck3biyzm0dllzqbipcrrzq8833dgv9w-python3.13-cffi-2.0.0/lib/python3.13/site-packages:/nix/store/9a6whjkar8lgcx4r7s1raghy8cx2qmvi-python3.13-pycparser-2.23/lib/python3.13/site-packages:/nix/store/zlyab7h640ndms7j1hddqg21qffjyg9h-python3.13-importlib-metadata-8.7.0/lib/python3.13/site-packages:/nix/store/01c584pblchs1mb8a8x8qv7nrqqmnj34-python3.13-zope-event-5.0/lib/python3.13/site-packages:/nix/store/kfqnq2yja6a8mpviddf0hwwbyj02lgrh-python3.13-zope-interface-7.2/lib/python3.13/site-packages:/nix/store/4yx6g5cmf7qdz3ma1kxyihh2qax5wiyl-python3.13-toml-0.10.2/lib/python3.13/site-packages:/nix/store/mbp694ghx6mxq688ki82zy0sh81f32xp-python3.13-zipp-3.23.0/lib/python3.13/site-packages:/nix/store/wq0qqmf5hb2mvihhjlqkn5f78df7z764-python3.13-packaging-25.0/lib/python3.13/site-packages:/nix/store/jqpbhxhfc5rn2s7vg2d0k27xgnay5w99-python3.13-python-dateutil-2.9.0.post0/lib/python3.13/site-packages:/nix/store/mmpgfjlkjmnmck321z2l02794hz4mh26-python3.13-tzdata-2025.2/lib/python3.13/site-packages:/nix/store/z120cd67469z5n44cpdyp4928kz5lmm5-python3.13-six-1.17.0/lib/python3.13/site-packages:/nix/store/4iaiqf1rgap1yycfn2hypk8z461b0jfk-python3.13-requests-2.33.1/lib/python3.13/site-packages:/nix/store/8qfwrsrj394j1fp4mvjdb6b1n41sd8n5-python3.13-certifi-2025.07.14/lib/python3.13/site-packages:/nix/store/r74s0kvqgk32yx74l4fi4fgvghlli0g5-python3.13-charset-normalizer-3.4.3/lib/python3.13/site-packages:/nix/store/ifjkkxadz3m6yfj8ldnbf732fh9h0xm8-python3.13-idna-3.11/lib/python3.13/site-packages:/nix/store/3yhphqykh9vhdaks6r68g1lkj8gs5b79-python3.13-urllib3-2.5.0/lib/python3.13/site-packages:/nix/store/w0x1yqwy7sgagkxs1kjxdd2myvw28gn6-python3.13-markupsafe-3.0.3/lib/python3.13/site-packages:/nix/store/q56lcwiczk03wvkhnrmgcqrgmrxc0y0p-python3.13-pyasn1-0.6.2/lib/python3.13/site-packages:/nix/store/89al2y3ivn1fc8r86zhsd8q442y5izsz-python3.13-pyasn1-modules-0.4.2/lib/python3.13/site-packages:/nix/store/gaj10yk8vl094knp2s4ah10z0xqfgx8l-oncall-0-unstable-2025-04-15/lib/python3.13/site-packages" + +nix-repl> inputs.node5-blog.outputs.packages.x86_64-linux.default.pythonPath +[ ] +``` + +the package from the UWSGI example exports a python path, mine does not, exmining +[the package from the UWSGI example](https://github.com/NixOS/nixpkgs/blob/nixos-25.11/pkgs/by-name/on/oncall/package.nix), +it defines the `pythonPath` by hand + +```nix +pythonPath = "${python3.pkgs.makePythonPath dependencies}:${oncall}/${python3.sitePackages}"; +``` + +Modifying my flake to export pythonPath aswell, by moving the pkg build to `let in`, and exposing it in the output. + +```nix +let + ... + + pkg = python.pkgs.buildPythonPackage (attrs // { + meta = { + description = "Blog backend for blog.node5.net"; + homepage = "https://blog.node5.net/Blog%20meta/"; + changelog = "https://git.node5.net/blog/blog.node5.net_flask/log/"; + mainProgram = "blog-node5"; + }; + }); +in +{ + packages.x86_64-linux.default = pkg; + pythonPath = "${python.pkgs.makePythonPath attrs.dependencies}:${pkg}/${python.sitePackages}"; +} +``` + +Success + +```nix-repl +nix-repl> outputs.pythonPath +"/nix/store/0r6k8xa2kgqyp3r4v2w7yrb80ma2iawm-python3-3.13.12/lib/python3.13/site-packages:/nix/store/10hk7srr12wgp2hqm5lai0xxr69m76b7-python3.13-flask-3.1.2/lib/python3.13/site-packages:/nix/store/jl0mxihyizv77l66mzbvmv49iiri72sd-python3.13-pyyaml-6.0.3/lib/python3.13/site-packages:/nix/store/pkj9yz58kijfwyg4c0xpwc2dlwwswr6s-python3.13-markdown-3.10.2/lib/python3.13/site-packages:/nix/store/6svr8x0lzmsn8d70asdc5qns35273216-python3.13-python-telegram-bot-22.7/lib/python3.13/site-packages:/nix/store/6snki2zk3rmh13wwi07g3x79a1rr032m-python3.13-pygments-2.20.0/lib/python3.13/site-packages:/nix/store/rfhv4bxzg6aqv7ll7d2g3fx7vdj63ks3-python3.13-tabulate-0.10.0/lib/python3.13/site-packages:/nix/store/77p6rnrhbc14aaw7iwf6d7vxl89qa9kj-python3.13-click-8.3.1/lib/python3.13/site-packages:/nix/store/8qn7dwv1rh0h80k7w0f9pa798y90vv2y-python3.13-blinker-1.9.0/lib/python3.13/site-packages:/nix/store/vxp23qrd7v308fr6g63cbai6lpxqm13j-python3.13-itsdangerous-2.2.0/lib/python3.13/site-packages:/nix/store/2kwicy8c1ab6zw8p1ps3nnn623b68dn0-python3.13-jinja2-3.1.6/lib/python3.13/site-packages:/nix/store/hmgasx01bmwlz4nr23gm13q9hnqkqw19-python3.13-werkzeug-3.1.6/lib/python3.13/site-packages:/nix/store/jpyvycfsc7gx267kaswq71dawa5ng0vq-python3.13-markupsafe-3.0.3/lib/python3.13/site-packages:/nix/store/r70kacvi02lxf71qmdhqqfjfbbzcr2pc-python3.13-httpx-0.28.1/lib/python3.13/site-packages:/nix/store/7y5zfyjwhqgxil8kq9qqsfbw00rmqzrn-python3.13-anyio-4.13.0/lib/python3.13/site-packages:/nix/store/hqpy59n4gai7vdd2wdzvgax6gjnk83wc-python3.13-certifi-2026.01.04/lib/python3.13/site-packages:/nix/store/hgsr99pnjk2bcjc4z3m0z6a76kgjnlyh-python3.13-httpcore-1.0.9/lib/python3.13/site-packages:/nix/store/ffl6rnq6adprav63d171av3v1a9c4a7x-python3.13-idna-3.11/lib/python3.13/site-packages:/nix/store/yz02xvcmxq8x69vdfhabqls4qpbi2n2h-python3.13-h11-0.16.0/lib/python3.13/site-packages:/nix/store/wlx6zqsn7sx3n005izf63gaigzp2wc1n-python3.13-blog.node5.net-0.1.0/lib/python3.13/site-packages" +``` + +Full blog flake: + +```nix +{ + description = "A basic flake using pyproject.toml project metadata"; + + inputs = { + pyproject-nix = { + url = "github:nix-community/pyproject.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { nixpkgs, pyproject-nix, ... }: + let + inherit (nixpkgs) lib; + + project = pyproject-nix.lib.project.loadPyproject { + # Read & unmarshal pyproject.toml relative to this project root. + # projectRoot is also used to set `src` for renderers such as buildPythonPackage. + projectRoot = ./.; + }; + + # This example is only using x86_64-linux + pkgs = nixpkgs.legacyPackages.x86_64-linux; + + python = pkgs.python3; + + # Returns an attribute set that can be passed to `buildPythonPackage`. + attrs = project.renderers.buildPythonPackage { inherit python; }; + + pkg = python.pkgs.buildPythonPackage (attrs // { + meta = { + description = "Blog backend for blog.node5.net"; + homepage = "https://blog.node5.net/Blog%20meta/"; + changelog = "https://git.node5.net/blog/blog.node5.net_flask/log/"; + mainProgram = "blog-node5"; + }; + }); + in + { + packages.x86_64-linux.default = + pkg; + } // { + pythonPath = "${python.pkgs.makePythonPath attrs.dependencies}:${pkg}/${python.sitePackages}"; + }; +} +``` + +We can use it like this: + +```nix +{ inputs, pkgs, ... }: +let + user = "blog"; + working_dir = "/var/lib/blog"; + db_location = "${working_dir}/blog.node5.net.db"; + + # Combine articles and other blog source files like templates and static files + content = pkgs.stdenv.mkDerivation { + pname = "node5-blog-content"; + version = "1.0"; + + src = "${inputs.blog-articles}"; + + buildPhase = '' + mkdir -p $out/articles + cp -a ${inputs.blog-articles}/* $out/articles/ + + mkdir -p $out/blog.node5.net + cp -a ${inputs.node5-blog}/blog.node5.net/* $out/ + ''; + }; +in +{ + users.extraUsers.${user} = { + isSystemUser = true; + description = "blog service user"; + home = "/nonexistent"; + shell = "/usr/sbin/nologin"; + group = "${user}"; + }; + + # https://nixos.wiki/wiki/Nginx#UNIX_socket_reverse_proxy + users.groups."${user}".members = [ "nginx" ]; + systemd.services.nginx.serviceConfig.ProtectHome = false; + + # Create project directories + systemd.tmpfiles.rules = [ + "d /run/blog 0770 blog nginx" + "d ${working_dir} 0771 blog uwsgi" + ]; + + # init DB if it doesn't exist + systemd.services."uwsgi".preStart = ''/bin/sh -c ' + if [ ! -f ${db_location} ]; + then + ${pkgs.sqlite}/bin/sqlite3 ${db_location} < ${inputs.node5-blog}/create_db.sql; + fi' + ''; + + services.uwsgi = { + enable = true; + plugins = [ "python3" ]; + instance = { + type = "emperor"; + vassals = { + blog = { + type = "normal"; + env = [ + "PYTHONPATH=${inputs.node5-blog.pythonPath}" + "CONTENT_ROOT_PATH=${content}" + ]; + module = "blog_node5_net:app"; + socket = "/run/blog/blog.sock"; + chdir = "${working_dir}"; # This is where the SQLite database will be stored + socketGroup = "nginx"; + immediate-gid = "nginx"; + chmod-socket = "770"; + buffer-size = 32768; + }; + }; + }; + }; + + services.nginx = { + enable = true; + virtualHosts."blog.node5.net" = { + enableACME = true; + forceSSL = true; + locations."/".uwsgiPass = "unix:/run/blog/blog.sock"; + }; + }; + +} +``` + +Minor things to improve, but it works + +--- + +## Firewall rejections are logged + +By default nix will log firewall rejections, you'll want to turn this off, to save your SSD + +networking.firewall.logRefusedConnections +[523935.720369] refused connection: IN=eno1 OUT= MAC=ec:8e:b5:73:ae:6b:22:55:a4:35:cd:8e:08:00 SRC=193.32.209.238 DST=45.145.93.105 LEN=40 TOS=0x00 PREC=0x20 TTL=56 ID=0 PROTO=TCP SPT=33401 DPT=28901 WINDOW=65535 RES=0x00 SYN URGP=0 + |
