From 0802a3305be33be03dc4615d7b85e76eb5c672ea Mon Sep 17 00:00:00 2001 From: "user@node5.net" Date: Sat, 30 May 2026 00:17:23 +0200 Subject: new article - Nix on my server deployment, front-page, blog done --- NixOS on my server/Thumbnail.webp | Bin 0 -> 44770 bytes NixOS on my server/index.md | 653 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 653 insertions(+) create mode 100644 NixOS on my server/Thumbnail.webp create mode 100644 NixOS on my server/index.md diff --git a/NixOS on my server/Thumbnail.webp b/NixOS on my server/Thumbnail.webp new file mode 100644 index 0000000..0007955 Binary files /dev/null and b/NixOS on my server/Thumbnail.webp differ diff --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) + +
+diff --git a/flake.nix b/flake.nix
+index a056d72..b47d632 100644
+--- a/flake.nix
++++ b/flake.nix
+@@ -23,9 +23,11 @@
+     };
+␠
+     node5-nvim.url = "git+https://git.node5.net/nix/nvim";
++
++    deploy-rs.url = "github:serokell/deploy-rs";
+   };
+␠
+-  outputs = { self, nixpkgs, nixpkgs-unstable, home-manager, ... } @ inputs:
++  outputs = { self, nixpkgs, nixpkgs-unstable, home-manager, deploy-rs, ... } @ inputs:
+     let
+       inherit (self) outputs;
+       system = "x86_64-linux";
+@@ -73,6 +75,32 @@
+           ];
+         };
+␠
++        node5-test = nixpkgs.lib.nixosSystem {
++          specialArgs = {inherit inputs unstable; };
++          modules = [
++            ./modules/hosts/node5-test/configuration.nix
++            ./modules/common.nix # nixos stuff I want on all machines
++          ];
++        };
++
+       };
++
++      deploy = {
++        nodes = {
++          node5-test = {
++            hostname = "192.168.1.63";
++            sshUser = "root";
++            profiles = {
++              system = {
++                user = "root";
++                path =
++                  deploy-rs.lib.${system}.activate.nixos
++                  self.nixosConfigurations.node5-test;
++              };
++            };
++          };
++        };
++      };
++
+     };
+ }
+
+ +
+❯ deploy .
+
+🚀 ℹ️ [deploy] [INFO] Running checks for flake in .
+warning: Git tree '/home/user/dot-files' is dirty
+warning: unknown flake output 'deploy'
+🚀 ℹ️ [deploy] [INFO] Evaluating flake in .
+warning: Git tree '/home/user/dot-files' is dirty
+🚀 ℹ️ [deploy] [INFO] 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] [INFO] Building profile `system` for node `node5-test`
+🚀 ℹ️ [deploy] [INFO] Copying profile `system` to node `node5-test`
+🚀 ℹ️ [deploy] [INFO] Activating profile `system` for node `node5-test`
+🚀 ℹ️ [deploy] [INFO] Creating activation waiter
+👀 ℹ️ [wait] [INFO] Waiting for confirmation event...
+⭐ ℹ️ [activate] [INFO] 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] [INFO] Activation succeeded!
+⭐ ℹ️ [activate] [INFO] Magic rollback is enabled, setting up confirmation hook...
+👀 ℹ️ [wait] [INFO] Found canary file, done waiting!
+⭐ ℹ️ [activate] [INFO] Waiting for confirmation event...
+🚀 ℹ️ [deploy] [INFO] Success activating, attempting to confirm activation
+🚀 ℹ️ [deploy] [INFO] Deployment confirmed.
+
+    ~/dot-files   master *4 +6 !5 ────────────────────────────────────  52s   impure  21:36:55
+
+ +It deploys a working config successfully, +now let's change the config such that we no longer have SSH access to the server + +
+-  networking.firewall.allowedTCPPorts = [ 22 ];
+
+ +
+❯ deploy .
+🚀 ℹ️ [deploy] [INFO] Running checks for flake in .
+warning: Git tree '/home/user/dot-files' is dirty
+warning: unknown flake output 'deploy'
+🚀 ℹ️ [deploy] [INFO] Evaluating flake in .
+warning: Git tree '/home/user/dot-files' is dirty
+🚀 ℹ️ [deploy] [INFO] 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] [INFO] Building profile `system` for node `node5-test`
+🚀 ℹ️ [deploy] [INFO] Copying profile `system` to node `node5-test`
+🚀 ℹ️ [deploy] [INFO] Activating profile `system` for node `node5-test`
+🚀 ℹ️ [deploy] [INFO] Creating activation waiter
+👀 ℹ️ [wait] [INFO] Waiting for confirmation event...
+⭐ ℹ️ [activate] [INFO] 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] [INFO] Activation succeeded!
+⭐ ℹ️ [activate] [INFO] Magic rollback is enabled, setting up confirmation hook...
+👀 ℹ️ [wait] [INFO] Found canary file, done waiting!
+⭐ ℹ️ [activate] [INFO] Waiting for confirmation event...
+🚀 ℹ️ [deploy] [INFO] Success activating, attempting to confirm activation
+⭐ ⚠️ [activate] [WARN] De-activating due to error
+switching profile from version 21 to 20
+⭐ ⚠️ [activate] [WARN] Removing generation by ID 21
+removing profile version 21
+⭐ ℹ️ [activate] [INFO] 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] [ERROR] 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] [INFO] Deployment confirmed.
+🚀 ❌ [deploy] [ERROR] Activating over SSH resulted in a bad exit code: RecvError(())
+🚀 ℹ️ [deploy] [INFO] Revoking previous deploys
+🚀 ❌ [deploy] [ERROR] 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
+
+ +--- + +
+🚀 ℹ️ [deploy] [INFO] Running checks for flake in /home/user/dot-files/
+warning: Git tree '/home/user/dot-files' is dirty
+warning: unknown flake output 'deploy'
+🚀 ℹ️ [deploy] [INFO] Evaluating flake in /home/user/dot-files/
+warning: Git tree '/home/user/dot-files' is dirty
+🚀 ℹ️ [deploy] [INFO] 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] [INFO] Building profile `system` for node `node5-test`
+🚀 ℹ️ [deploy] [INFO] Copying profile `system` to node `node5-test`
+🚀 ℹ️ [deploy] [INFO] Activating profile `system` for node `node5-test`
+🚀 ℹ️ [deploy] [INFO] Creating activation waiter
+🚀 ℹ️ [deploy] [INFO] Success activating, attempting to confirm activation
+⭐ ℹ️ [activate] [INFO] Activating profile
+🚀 ℹ️ [deploy] [INFO] Deployment confirmed.
+stopping the following units: wg-quick-wg0.service
+
+
+ + +
+diff --git a/modules/hosts/node5-test/wireguard.nix b/modules/hosts/node5-test/wireguard.nix
+index 288f7ae..ad86695 100644
+--- a/modules/hosts/node5-test/wireguard.nix
++++ b/modules/hosts/node5-test/wireguard.nix
+@@ -7,18 +7,24 @@ in
+     allowedUDPPorts = [ listenPort ];
+     interfaces."wg0".allowedTCPPorts = [ 22 ]; # SSH from personal systems
+   };
+-  networking.wg-quick.interfaces = {
++  networking.wireguard.interfaces = {
+     wg0 = {
+-      address = [ "10.10.41.1/24" ];
++      ips = [ "10.10.41.1/24" ];
+       privateKeyFile = "/etc/secrets/wireguard/privatekey";
+       listenPort = listenPort;
+       peers = [
+         {
+-          # T480s
++          name = "T480s";
+           publicKey = "YYjWG9lD4zkjNkjMYH4CfIac1sqsWZknWFh6d4OxmnM=";
+           presharedKeyFile = "/etc/secrets/wireguard/t480s_presharedkey";
+-          allowedIPs = [ "10.10.41.110/24" ];
++          allowedIPs = [ "10.10.41.110/32" ];
+         }
+       ];
+     };
+   };
+
+ +> 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 + +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"; + }; + }); + }; +} +``` + +
+result
+├── bin
+│   └── blog-node5
+├── lib
+│   └── python3.13
+│       └── site-packages
+│           ├── __pycache__
+│           │   ├── article.cpython-313.opt-1.pyc
+│           │   ├── article.cpython-313.pyc
+│           │   ├── blog_node5_net.cpython-313.opt-1.pyc
+│           │   ├── blog_node5_net.cpython-313.pyc
+│           │   ├── db_handler.cpython-313.opt-1.pyc
+│           │   ├── db_handler.cpython-313.pyc
+│           │   ├── telegram_handler.cpython-313.opt-1.pyc
+│           │   └── telegram_handler.cpython-313.pyc
+│           ├── blog_node5_net-0.1.0.dist-info
+│           │   ├── entry_points.txt
+│           │   ├── METADATA
+│           │   ├── RECORD
+│           │   ├── top_level.txt
+│           │   └── WHEEL
+│           ├── article.py
+│           ├── blog_node5_net.py
+│           ├── db_handler.py
+│           └── telegram_handler.py
+└── nix-support
+    └── propagated-build-inputs
+
+ + +`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 + -- cgit 1.4.1