···11+---
22+title: I was Wrong about Nix
33+date: 2020-02-10
44+tags:
55+ - nix
66+ - witchcraft
77+---
88+99+From time to time, I am outright wrong on my blog. This is one of those times.
1010+In my [last post about Nix][nixpost], I didn't see the light yet. I think I do
1111+now, and I'm going to attempt to clarify below.
1212+1313+[nixpost]: https://christine.website/blog/thoughts-on-nix-2020-01-28
1414+1515+Let's talk about a more simple scenario: writing a service in Go. This service
1616+will depend on at least the following:
1717+1818+- A Go compiler to build the code into a binary
1919+- An appropriate runtime to ensure the code will run successfully
2020+- Any data files needed at runtime
2121+2222+A popular way to model this is with a Dockerfile. Here's the Dockerfile I use
2323+for my website (the one you are reading right now):
2424+2525+```
2626+FROM xena/go:1.13.6 AS build
2727+ENV GOPROXY https://cache.greedo.xeserv.us
2828+COPY . /site
2929+WORKDIR /site
3030+RUN CGO_ENABLED=0 go test -v ./...
3131+RUN CGO_ENABLED=0 GOBIN=/root go install -v ./cmd/site
3232+3333+FROM xena/alpine
3434+EXPOSE 5000
3535+WORKDIR /site
3636+COPY --from=build /root/site .
3737+COPY ./static /site/static
3838+COPY ./templates /site/templates
3939+COPY ./blog /site/blog
4040+COPY ./talks /site/talks
4141+COPY ./gallery /site/gallery
4242+COPY ./css /site/css
4343+HEALTHCHECK CMD wget --spider http://127.0.0.1:5000/.within/health || exit 1
4444+CMD ./site
4545+```
4646+4747+This fetches the Go compiler from [an image I made][godockerfile], copies the
4848+source code to the image, builds it (in a way that makes the resulting binary a
4949+[static executable][staticbin]), and creates the runtime environment for it.
5050+5151+[godockerfile]: https://github.com/Xe/dockerfiles/blob/master/lang/go/Dockerfile
5252+[staticbin]: https://oddcode.daveamit.com/2018/08/16/statically-compile-golang-binary/
5353+5454+Let's let it build and see how big the result is:
5555+5656+```
5757+$ docker build -t xena/christinewebsite:example1 .
5858+<output omitted>
5959+$ docker images | grep xena
6060+xena/christinewebsite example1 4b8ee64969e8 24 seconds ago 111MB
6161+```
6262+6363+Investigating this image with [dive][dive], we see the following:
6464+6565+[dive]: https://github.com/wagoodman/dive
6666+6767+- The package manager is included in the image
6868+- The package manager's database is included in the image
6969+- An entire copy of the C library is included in the image (even though the
7070+ binary was _statically linked_ to specifically avoid this)
7171+- Most of the files in the docker image are unrelated to my website's
7272+ functionality and are involved with the normal functioning of Linux systems
7373+7474+Granted, [Alpine Linux][alpine] does a good job at keeping this chaff to a
7575+minimum, but it is still there, still needs to be updated (causing all of my
7676+docker images to be rebuilt and applications to be redeployed) and still takes
7777+up space in transfer quotas and on the disk.
7878+7979+[alpine]: https://alpinelinux.org
8080+8181+Let's compare this to the same build process but done with Nix. My Nix setup is
8282+done in a few phases. First I use [niv][niv] to manage some dependencies a-la
8383+git submodules that don't hate you:
8484+8585+[niv]: https://github.com/nmattia/niv
8686+8787+```
8888+$ nix-shell -p niv
8989+[nix-shel]$ niv init
9090+<writes nix/*>
9191+```
9292+9393+Now I add the tool [vgo2nix][vgo2nix] in niv:
9494+9595+[vgo2nix]: https://github.com/adisbladis/vgo2nix
9696+9797+```
9898+[nix-shell]$ niv add adisbladis/vgo2nix
9999+```
100100+101101+And I can use it in my shell.nix:
102102+103103+```nix
104104+let
105105+ pkgs = import <nixpkgs> { };
106106+ sources = import ./nix/sources.nix;
107107+ vgo2nix = (import sources.vgo2nix { });
108108+in pkgs.mkShell { buildInputs = [ pkgs.go pkgs.niv vgo2nix ]; }
109109+```
110110+111111+And then relaunch nix-shell with vgo2nix installed and convert my [go modules][gomod]
112112+dependencies to a Nix expression:
113113+114114+[gomod]: https://github.com/golang/go/wiki/Modules
115115+116116+```
117117+$ nix-shell
118118+<some work is done to compile things, etc>
119119+[nix-shell]$ vgo2nix
120120+<writes deps.nix>
121121+```
122122+123123+Now that I have this, I can follow the [buildGoPackage
124124+instructions][buildgopackage] from the upstream nixpkgs documentation and create
125125+`site.nix`:
126126+127127+[buildgopackage]: https://nixos.org/nixpkgs/manual/#ssec-go-legacy
128128+129129+```
130130+{ pkgs ? import <nixpkgs> {} }:
131131+with pkgs;
132132+133133+assert lib.versionAtLeast go.version "1.13";
134134+135135+buildGoPackage rec {
136136+ name = "christinewebsite-HEAD";
137137+ version = "latest";
138138+ goPackagePath = "christine.website";
139139+ src = ./.;
140140+141141+ goDeps = ./deps.nix;
142142+ allowGoReference = false;
143143+ preBuild = ''
144144+ export CGO_ENABLED=0
145145+ buildFlagsArray+=(-pkgdir "$TMPDIR")
146146+ '';
147147+148148+ postInstall = ''
149149+ cp -rf $src/blog $bin/blog
150150+ cp -rf $src/css $bin/css
151151+ cp -rf $src/gallery $bin/gallery
152152+ cp -rf $src/static $bin/static
153153+ cp -rf $src/talks $bin/talks
154154+ cp -rf $src/templates $bin/templates
155155+ '';
156156+}
157157+```
158158+159159+And this will do the following:
160160+161161+- Download all of the needed dependencies and place them in the system-level Nix
162162+ store so that they are not downloaded again
163163+- Set the `CGO_ENABLED` environment variable to `0` so the Go compiler emits a
164164+ static binary
165165+- Copy all of the needed files to the right places so that the blog, gallery and
166166+ talks features can load all of their data
167167+- Depend on nothing other than a working system at runtime
168168+169169+This Nix build manifest doesn't just work on Linux. It works on my mac too. The
170170+dockerfile approach works great for Linux boxes, but (unlike what the me of a
171171+decade ago would have hoped) the whole world just doesn't run Linux on their
172172+desktops. The real world has multiple OSes and Nix allows me to compensate.
173173+174174+So, now that we have a working _cross-platform_ build, let's see how big it
175175+comes out as:
176176+177177+```
178178+$ readlink ./result-bin
179179+/nix/store/ayvafpvn763wwdzwjzvix3mizayyblx5-christinewebsite-HEAD-bin
180180+$ du -hs result-bin/
181181+89M ./result-bin/
182182+$ du -hs result-bin/
183183+11M ./result-bin/bin
184184+888K ./result-bin/blog
185185+40K ./result-bin/css
186186+44K ./result-bin/gallery
187187+77M ./result-bin/static
188188+28K ./result-bin/talks
189189+64K ./result-bin/templates
190190+```
191191+192192+As expected, most of the build results are static assets. I have a lot of larger
193193+static assets including an entire copy of TempleOS, so this isn't too
194194+surprising. Let's compare this to on the mac:
195195+196196+```
197197+$ du -hs result-bin/
198198+ 91M result-bin/
199199+$ du -hs result-bin/*
200200+ 14M result-bin/bin
201201+872K result-bin/blog
202202+ 36K result-bin/css
203203+ 40K result-bin/gallery
204204+ 77M result-bin/static
205205+ 24K result-bin/talks
206206+ 60K result-bin/templates
207207+```
208208+209209+Which is damn-near identical save some macOS specific crud that Go has to deal
210210+with.
211211+212212+I mentioned this is used for Docker builds, so let's make `docker.nix`:
213213+214214+```nix
215215+{ system ? builtins.currentSystem }:
216216+217217+let
218218+ pkgs = import <nixpkgs> { inherit system; };
219219+220220+ callPackage = pkgs.lib.callPackageWith pkgs;
221221+222222+ site = callPackage ./site.nix { };
223223+224224+ dockerImage = pkg:
225225+ pkgs.dockerTools.buildImage {
226226+ name = "xena/christinewebsite";
227227+ tag = pkg.version;
228228+229229+ contents = [ pkg ];
230230+231231+ config = {
232232+ Cmd = [ "/bin/site" ];
233233+ WorkingDir = "/";
234234+ };
235235+ };
236236+237237+in dockerImage site
238238+```
239239+240240+And then build it:
241241+242242+```
243243+$ nix-build docker.nix
244244+<output omitted>
245245+$ docker load -i result
246246+c6b1d6ce7549: Loading layer [==================================================>] 95.81MB/95.81MB
247247+$ docker images | grep xena
248248+xena/christinewebsite latest 0d1ccd676af8 50 years ago 94.6MB
249249+```
250250+251251+And the output is 16 megabytes smaller.
252252+253253+The image age might look weird at first, but it's part of the reproducibility
254254+Nix offers. The date an image was built is something that can change with time
255255+and is actually a part of the resulting file. This means that an image built one
256256+second after another has a different cryptographic hash. It helpfully pins all
257257+images to Unix timestamp 0, which just happens to be about 50 years ago.
258258+259259+Looking into the image with `dive`, the only packages installed into this image
260260+are:
261261+262262+- The website and all of its static content goodness
263263+- IANA portmaps that Go depends on as part of the [`net`][gonet] package
264264+- The standard list of [MIME types][mimetypes] that the [`net/http`][gonethttp]
265265+ package needs
266266+- Time zone data that the [`time`][gotime] package needs
267267+268268+[gonet]: https://godoc.org/net
269269+[gonethttp]: https://godoc.org/net/http
270270+[gotime]: https://godoc.org/time
271271+272272+And that's it. This is _fantastic_. Nearly all of the disk usage has been
273273+eliminated. If someone manages to trick my website into executing code, that
274274+attacker cannot do anything but run more copies of my website (that will
275275+immediately fail and die because the port is already allocated).
276276+277277+This strategy pans out to more complicated projects too. Consider a case where a
278278+frontend and backend need to be built and deployed as a unit. Let's create a new
279279+setup using niv:
280280+281281+```
282282+$ niv init
283283+```
284284+285285+Since we are using [Elm][elm] for this complicated project, let's add the
286286+[elm2nix][elm2nix] tool so that our Elm dependencies have repeatable builds, and
287287+[gruvbox-css][gcss] for some nice simple CSS:
288288+289289+[elm]: https://elm-lang.org
290290+[elm2nix]: https://github.com/cachix/elm2nix
291291+[gcss]: https://github.com/Xe/gruvbox-css
292292+293293+```
294294+$ niv add cachix/elm2nix
295295+$ niv add Xe/gruvbox-css
296296+```
297297+298298+And then add it to our `shell.nix`:
299299+300300+```
301301+let
302302+ pkgs = import <nixpkgs> {};
303303+ sources = import ./nix/sources.nix;
304304+ elm2nix = (import sources.elm2nix { });
305305+in
306306+pkgs.mkShell {
307307+ buildInputs = [
308308+ pkgs.elmPackages.elm
309309+ pkgs.elmPackages.elm-format
310310+ elm2nix
311311+ ];
312312+}
313313+```
314314+315315+And then enter `nix-shell` to create the Elm boilerplate:
316316+317317+```
318318+$ nix-shell
319319+[nix-shell]$ cd frontend
320320+[nix-shell:frontend]$ elm2nix init > default.nix
321321+[nix-shell:frontend]$ elm2nix convert > elm-srcs.nix
322322+[nix-shell:frontend]$ elm2nix snapshot
323323+```
324324+325325+And then we can edit the generated Nix expression:
326326+327327+```
328328+let
329329+ sources = import ./nix/sources.nix;
330330+ gcss = (import sources.gruvbox-css { });
331331+# ...
332332+ buildInputs = [ elmPackages.elm gcss ]
333333+ ++ lib.optional outputJavaScript nodePackages_10_x.uglify-js;
334334+# ...
335335+ cp -rf ${gcss}/gruvbox.css $out/public
336336+ cp -rf $src/public/* $out/public/
337337+# ...
338338+ outputJavaScript = true;
339339+```
340340+341341+And then test it with `nix-build`:
342342+343343+```
344344+$ nix-build
345345+<output omitted>
346346+```
347347+348348+And now create a `name.nix` for your Go service like I did above. The real
349349+magic comes from the `docker.nix` file:
350350+351351+```
352352+{ system ? builtins.currentSystem }:
353353+354354+let
355355+ pkgs = import <nixpkgs> { inherit system; };
356356+ sources = import ./nix/sources.nix;
357357+ backend = import ./backend.nix { };
358358+ frontend = import ./frontend/default.nix { };
359359+in
360360+361361+pkgs.dockerTools.buildImage {
362362+ name = "xena/complicatedservice";
363363+ tag = "latest";
364364+365365+ contents = [ backend frontend ];
366366+367367+ config = {
368368+ Cmd = [ "/bin/backend" ];
369369+ WorkingDir = "/public";
370370+ };
371371+};
372372+```
373373+374374+Now both your backend and frontend services are built with the dependencies in
375375+the Nix store and shipped as a repeatable Docker image.
376376+377377+Sometimes it might be useful to ship the dependencies to a service like
378378+[Cachix][cachix] to help speed up builds.
379379+380380+[cachix]: https://cachix.org
381381+382382+You can install the cachix tool like this:
383383+384384+```
385385+$ nix-env -iA cachix -f https://cachix.org/api/v1/install
386386+```
387387+388388+And then follow the steps at [cachix.org][cachix] to create a new binary cache.
389389+Let's assume you made a cache named `teddybear`. When you've created a new
390390+cache, logged in with an API token and created a signing key, you can pipe
391391+nix-build to the Cachix client like so:
392392+393393+```
394394+$ nix-build | cachix push teddybear
395395+```
396396+397397+And other people using that cache will benefit from your premade dependency and
398398+binary downloads.
399399+400400+To use the cache somewhere, install the Cachix client and then run the
401401+following:
402402+403403+```
404404+$ cachix use teddybear
405405+```
406406+407407+I've been able to use my Go, Elm, Rust and Haskell dependencies on other
408408+machines using this. It's saved so much extra download time.
409409+410410+## tl;dr
411411+412412+I was wrong about Nix. It's actually quite good once you get past the
413413+documentation being baroque and hard to read as a beginner. I'm going to try and
414414+do what I can to get the documentation improved.
415415+416416+As far as getting started with Nix, I suggest following these posts:
417417+418418+- Nix Pills: https://nixos.org/nixos/nix-pills/
419419+- Nix Shorts: https://github.com/justinwoo/nix-shorts
420420+- NixOS: For Developers: https://myme.no/posts/2020-01-26-nixos-for-development.html
421421+422422+Also, I really suggest trying stuff as a vehicle to understand how things work.
423423+I got really far by experimenting with getting [this Discord bot I am writing in
424424+Rust][withinbot] working in Nix and have been very pleased with how it's turned
425425+out. I don't need to use `rustup` anymore to manage my Rust compiler or the
426426+language server. With a combination of [direnv][direnv] and [lorri][lorri], I
427427+can avoid needing to set up language servers or the like _at all_. I can define
428428+them as part of the _project environment_ and then trust the tools I build on
429429+top of to take care of that for me.
430430+431431+Give Nix a try. It's worth at least that much in my opinion.
+4
blog/thoughts-on-nix-2020-01-28.markdown
···991010# Thoughts on Nix
11111212+EDIT(M02 20 2020): I've written a bit of a rebuttal to my own post
1313+[here](https://christine.website/blog/i-was-wrong-about-nix-2020-02-10). I am
1414+keeping this post up for posterity.
1515+1216I don't really know how I feel about [Nix][nix]. It's a functional package
1317manager that's designed to help with dependency hell. It also lets you define
1418packages using [Nix][nixlang], which is an identically named yet separate thing.