I just migrated my server to NixOS. While not sure if it is ready for serious production, I agree on their philosophy and really like its direction. Here is my nix configurations.

NixOS is an operating system that manage packages and configurations by Nix, the package manager. It introduces itself as "The Purely Functional Package Manager", but I would rather call it "An Immutable Package Manager" for easier understanding.

The basic idea is: For the same input, you should always get the same immutable output. If it works now, it always works.

By having no side effect, it is possible have multiple versions of the same package running on the same OS, they will have their own dependency sorted out without breaking anything. This also makes downgrading super easy, if you upgraded something and find it doesn't work, nix-env --rollback will roll you back to the previous version, nothing breaks. It is more amazining when upgrading NixOS, because the entire OS is built and managed by Nix, downgrading the whole OS is just 1 command nixos-rebuild switch --rollback, that includes everything from third party softwares to kernel modules, how safe!

How to configure?

The reproducibility does not come for free, you have to follow NixOS's way to configure your packages. For example if you are customising vim using ~/.vimrc, Nix is not going to manage that file, changing that file will make vim behave differently, even the package itself is not changed. The solution would be using Nix to manage configurations.

Every package is built from a derivation, but instead of only providing derivations, Nix gives you functions that return derivations, allowing you to customise packages by passing arguments to those functions. Here is an example of customising vim, written in Nix-the-langauge.

nixpkgs.vim_configurable.customize {
    name = "vim";
    vimrcConfig.customRC = ''
      set nocompatible
      set backspace=indent,eol,start
    '';
}

The code above is a Nix expression calling the nixpkgs.vim_configurable.customize function, passing in 2 arguments, name and vimrcConfig.customRC. You can read more about the language from the manual, it is a dynamic, lazily evaluated, functional language. This will build a package with a script called "vim":

#!/bin/sh
exec /nix/store/q6yhcy7g8107x9pdf8mi8fp0cf7rin33-vim_configurable-7.4.826/bin/vim -u /nix/store/ab3ghyb857y33zvngkz3i0rrmji17hrx-vimrc  "$@"

As you may have guessed /nix/store/<hash>-vim_configurable-7.4.826/bin/vim is the original vim, and /nix/store/<hash>-vimrc is a text file that contains the vimrc that I passed to the function. By running the vim command, you are not running the original vim, but a shell script that injects flags to vim. Configuring other packages are very much the same, look at the Nix expressions, find a useful argument, then pass things into the function.

As you can see, there is a very explicit dependency (that absolute path with hash) between the packages, Nix can figure out the dependency tree by looking at the files, and therefore can provide handle functions like nix-collect-garbage, removing packages that no one uses.

Development environment

It is also fun to use Nix as development environment. Many languages I used has some kind of "version management" or "virtual environment", may it be nvm, rvm, python's virtual environment, or cabal sandbox. The goal is to make sure the environment for the project to be the same, unfortunately none them covers every corner you need. Let's say you want to use imagemagic with nodejs, you will have to run this:

brew install imagemagick
brew install graphicsmagick
npm install gm

Homebrew does not manage nodejs packages, npm does not manage system packages, so you have to handle them yourself. Here's where Nix shines, Nix does both! You can specific your system dependency and nodejs dependency at the same place, and running nix-shell should get you into an environment that is ready to build. I also met someone doing nodejs, python, clojure at a brilliant meetup, he uses Nix just for development!

It worths mentioning that Stack has Nix integration build in, which means you should be able to manage your Haskell project's system dependency with Stack in a flawless way.

Managing project dependencies

Instead of just managing system packages, Nix is also capable of managing project dependencies, here it overlaps those language specific package managers like cabal and pip. The way it does it, is adding those packages into Nix's channel, for example you can find haskellPackages.tagsoup and python3.3-pyramid available. The repository is not very complete and you may need to contribute if you find something you need is not there.

There is another way to handle this, is by converting existing dependencies into Nix expressions. I setup my blog which is running on nodejs, by converting package.json to some Nix expressions, using npm2nix. The generated expression is pretty straightforward, it is just putting each dependency into its own Nix package, then download them into the node_modules folder. After reading how others setup their ghost blog, I am able to get mine running too!

I am surprised there are some tools converting different languages' package to nix expression: erlang / elixir, go, python.

VS Docker

Nix does not replace Docker, but it solves similar issues with a better solution. You can get deterministic build, easy deployment, neat dependency tree with Nix, no need to download an OS just for one process, configuration works pretty much the same too (except you have to learn Nix-the-language).

However Nix does not provide the isolation Docker has. A process is just like every other ordinary process that can read and write files with the given user permission, there is no sandbox, ports bind to system directly so you cannot hide them (well, not exposing them) like you can in Docker.

Thoughts

I think Nix is trying to solve a problem at a fundamental level, instead of avoiding the problem by putting things into containers like Docker. However, this requires a more careful design for build process, lots of package does not follow Nix's philosophy, therefore requires patching before / after build, that patch can be dirty.

Is it really pure? No. You can get around easily by running curl some-website | sh in the build script. There is no 100% enforcement to pureness, people try their best to maintain pureness by code review / impure path detections / etc.

Nix (the language) also make it more difficult to start with. On other systems, you just need to run a command then something will be installed somewhere, you have no clue what is happening, but it kind of works, configuration is not centralised nor managed, but at least it works. On Nix, you need to understand the language before customising anything, learning a functional programming language can be hard, especially when Nix is not popular. Then you have to explore the nixpkgs repo to look for useful functions (and its arguments), which is definitely not something you would do with a traditional package manager. At least I never tried to understand what apt-get is actually doing.

Difficulties

  • When I made my first package, mocking the environment costed most of my time. For example I have to fake and create a HOME holder because mix, npm and stack looks for that.
  • Setting runtime dependency for packages is not straightforward, there is a wrapProgram script from the makeWrapper package, I wish I found that earlier.
  • It is interesting that some tools I used everyday is not available without explicitly specifying them as build input, such as ps (procps) and bash.
  • Mix (a build tool for Elixir) release generate startup script that mutate the current folder by default, I have to dig through source code to find the RELEASE_MUTABLE_DIR flag, and change the path.
  • I wasn’t aware of the build-in buildStackProject function, it makes building Haskell project much easier
  • I used up all storage on the machine, because of tmp is filling up.
    • Building package with the -K flag will retains the build directory in the tmp folder, and it retains all dependency. I used it for debug purpose.
    • NixOS has boot.cleanTmpDir = false by default, which means storage fill up pretty fast if I keep developing with the -K flag.
  • I am running a web server with the Yesod framework, it creates client_session_key.aes in the current directory, which of course fails in Nix's immutable path.
    • I finally give up and copy everything to a mutable direct on the first launch.
  • Some build script does not fit Nix's model. Running grunt for Ghost trigger git submodules operation, which is not pure.
    • I ended up downloading Ghost's prebuilt zip.
  • Installed NixOS 16.09 on both XEN and KVM Linode's machines, 1 of them has interface named eth0, and the other is enp0s4, I thought NixOS should make sure everything remains the same
  • I followed the instruction to install NixOS on Linode's machine, but machines at Tokyo data centre cannot upgrade to the newer KVM machine, which means the tutorial does not work 100%. I have to figure out how to install by looking at the wiki's history before Linode migrate from XEN to KVM. This is not NixOS's issue, but using something minor means lack support, lack of tutorial.
  • I cannot install NixOS 16.09 directly, need to install an older version first and then upgrade due to this bug.
  • Nix integrates with different systems, and it brings conventions from other systems into Nix's code, nix file can look really inconsistent.
    • Transmission setting is: roc-enabled
    • Systemd setting is: ExecStart
    • Environment variables are ALL_CAPITAL

Links

Here are some useful links I find it useful when dealing with NixOS, hopefully help someone

Blog Logo

b123400


Published