NixOS on my server
Updating over wireguard without cutting the branch you're sitting on, migrating services
![]()
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.
[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
[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 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
Here's a nice deploy-rs setup guide - crystalwobsite.gay
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,/24will 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
- Unpack: handles the preparation of the build environment (e.g. extracting archives, touching files, etc.);
- Patch: handles changes to the underlying source code (e.g. patching bugs, adapting the source code to work in a Nix environment, etc.);
- Configure: handles the configuration of the build environment, for example detecting system capabilities and setting build parameters;
- Build: compiles the source code into binaries, bytecode, or otherwise a distributable form of the source code;
- Check: performs any tests on the compiled package, for example the package's test suite;
- Install: copies the build artefacts to the output directory, handling any needed changes (e.g. directory structure reorganisation);
- Fixup: process the output artefacts to work in a Nix environment (e.g.: strip binaries, override ELF paths, handle dynamic library linking, etc.);
- Install Check: performs any tests on the final output, essentially acting as a integration test into the Nix environment;
- Dist: creates distribution archives (rarely used).
Read more: source - wiki.nixos.org
{ 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 Python - wiki.nixos.org 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
node5-blog.url = "git+https://git.node5.net/blog/blog.node5.net_flask";
configuration.nix
environment.systemPackages = with pkgs; [
inputs.node5-blog.packages.x86_64-linux.default
]
{
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
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,
it defines the pythonPath by hand
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.
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> 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:
{
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:
{ 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
Comments