--- description: Updating over wireguard without cutting the branch you're sitting on, migrating services created: 2026-05-09 --- ![❄️](Thumbnail.webp) 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 This server is 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 ```bash 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) #### Test server Let's try it out on a test server
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
Success! ``` 🚀 ❌ [deploy] [ERROR] Deployment to node node5-test failed, rolled back to previous generation ``` #### Prod server wireguard Cool, let's ship it to prod 🚢
🚀 ℹ️ [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

Bollocks, it still takes down the wireguard service as part of the deployment, and doesn't recover automatically. Solution: switch from wg-quick to native wireguard.
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