So nix is kind of weird thing, because parts of it you can use to replace part of components but hen you can create a whole solution (and ultimately that works better). In that case it's like asking to write a tutorial for python.

To illustrate what I mean here is my history with nix (simplified, but it's meant to show the evolution):

So I started using nix as a reproductive dev environment. Initially using shell.nix to have all my tooling. This is great if you also have direnv installed, which basically automatically makes tools related to the project to appear.

Anyway I started then using various tools that would translate python project to nix initially was also a big fan of setup.py/cfg and pip-tools. I wasn't happy with either of them until I found poetry2nix. Poetry is also quite decent so now that's my favorite way to build a project.

So what poetry2nix does is translates on the fly pyproject.toml and poetry.lock file to nix expression. With it you can build project in nix you can also create a shell that has python with all packages available as described by poetry. So that's now quite decent reproducible environment.

I realized that to make the dev environment nice I'm adding a lot of nix boilerplate to every project, also as I'm learning new things and improving it it is getting hard to synchronize that boilerplate across other projects.

So I created a new repo where I put everything there, I also learned how to use modules system (which they created for NixOS) ultimately I made repo that you reference in default.nix and shell.nix in my project it loads project.nix file which describes information about my project. Allows me to set various aspect of the application, like what additional packages should be installed in dev shell (maybe I need a local postgresql installation). How the project will be deployed (if it is meant to run as AWS lambda, a serverless (npm application) is added and package.json/package-lock.json files are sourced. If it is a docker asks what binary should be executed in the container. Whether to compile using minified python (with missing some of core libraries) etc.

That ended to be some Nix code, but simplified things greatly, and makes things usable by people not familiar with Nix.

We use gitlab for CI/CD so I created a special gitlab-runner for nix builds. It actually is already built in in NixOS and only needs to be enabled, and they also show configuration in example. Basically how it works is that it stills spins a docker container for the build, but exposes /nix/store (read only) and the unix socket to trigger build. I also created an S3 bucket and configured it for storing caches of builds.

Having this runner lets me utilize one of major benefits of Nix which is caching (if all inputs (source files, architecture, interpreters etc) don't change then output will be the same). This is great, because if I only change source file, and don't change dependencies, the pipeline will only rebuild my app. If I work on a branch and branch passes all tests, I merge it and merge is just fast forward, then build job in master branch just takes couple seconds, because nothing change. If the branch wasn't rebased and actual merge was done and some files are now different the nix will rebuild things that were modified.

Deployment of my python app as a lambda ends up being just:

nix-shell --run "sls deploy"

nix-shell takes care that the dev environment is created and inside of it invokes "sls deploy" command. This similarly utilizes caching (that persists between builds) so it is faster than what we normally used like loading a docker container and then running it.

This is quite decent and I'm still testing it on a single app, but it basically cut deployment time from 10 minutes to 5 minutes. And I know I can cut it further (serverless doesn't know about nix, but it understands poetry, so once again again it downloads all dependencies it needs, extracts them and bundles them into the lambda. I plan to populate its cache from the packages that nix stores when is running the build).

There is also possibility that I'm aware of, but haven't explored yet, but seems interesting. It might require some work. For example you could configure your local computer to use the machine that is used for CI/CD build to help with local builds. Similarly you could expose the cache as well to prevent unnecessary rebuilds. You probably shouldn't do that if the user isn't trustworthy, otherwise you might need to create some additional safety layer.

Now in your scenario you have bare bone servers to deploy. If those would run NixOS I believe the deployment step would be simply to deliver new configuration.nix referencing your application to them and also have those machines configured to use the same cache as you had with CI/CD so they won't have to rebuild everything again. There are several tools that make the process easier, for people who use public cloud there's terraform support, NixOps (last time I used it was the weakest part of Nix, but maybe it improved, it was great tool when I wanted to test everything a la hashicorp's vagrant)