summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--NixOS on my server/Thumbnail.webpbin0 -> 44770 bytes
-rw-r--r--NixOS on my server/index.md653
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 = &quot;git+https://git.node5.net/nix/nvim&quot;;
+<span style="color:lime;">+</span>
+<span style="color:lime;">+</span><span style="color:lime;">    deploy-rs.url = &quot;github:serokell/deploy-rs&quot;;</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 = &quot;x86_64-linux&quot;;
+<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 = &quot;192.168.1.63&quot;;</span>
+<span style="color:lime;">+</span><span style="color:lime;">            sshUser = &quot;root&quot;;</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 = &quot;root&quot;;</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 = &quot;root&quot;
+ssh_user = &quot;root&quot;
+path = &quot;/nix/store/z8c3vc2689lbcvplhs42iqzbbb7x7k9s-activatable-nixos-system-node5-25.11.20260415.1766437&quot;
+hostname = &quot;192.168.1.63&quot;
+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 = &quot;root&quot;
+ssh_user = &quot;root&quot;
+path = &quot;/nix/store/2dfsx5blqqib25ir0v32azqn2g49d267-activatable-nixos-system-node5-25.11.20260415.1766437&quot;
+hostname = &quot;192.168.1.63&quot;
+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.&quot;wg0&quot;.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 = [ &quot;10.10.41.1/24&quot; ];</span>
+<span style="color:lime;">+</span><span style="color:lime;">      ips = [ &quot;10.10.41.1/24&quot; ];</span>
+       privateKeyFile = &quot;/etc/secrets/wireguard/privatekey&quot;;
+       listenPort = listenPort;
+       peers = [
+         {
+<span style="color:red;">-          # T480s</span>
+<span style="color:lime;">+</span><span style="color:lime;">          name = &quot;T480s&quot;;</span>
+           publicKey = &quot;YYjWG9lD4zkjNkjMYH4CfIac1sqsWZknWFh6d4OxmnM=&quot;;
+           presharedKeyFile = &quot;/etc/secrets/wireguard/t480s_presharedkey&quot;;
+<span style="color:red;">-          allowedIPs = [ &quot;10.10.41.110/24&quot; ];</span>
+<span style="color:lime;">+</span><span style="color:lime;">          allowedIPs = [ &quot;10.10.41.110/32&quot; ];</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
+