Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

It's only setting file contents and folder generation, that's still declarative.


Genuine question, how is that more declarative than

    FROM nginx


Recall, the Dockerfile syntax is 'imperative'. If we change the order of the commands in the Dockerfile, we likely end up with a different image.

In the Nix example, the image we build is in the expression `nix2container.buildImage { ... }`.

The `nginxWebRoot` is the package with the index.html:

    nginxWebRoot = pkgs.writeTextDir "index.html" ''
      <html><body><h1>Hello from NGINX</h1></body></html>
    '';
It's reasonable to say "writeTextDir" modifies the disk. I don't think it's reasonable to say just because changes in state occur that the code is imperative. (e.g. SQL is a declarative language, but clearly allows modifying the database).

Similarly:

    nginxVar = pkgs.runCommand "nginx-var" {} ''
      mkdir -p $out/var/log/nginx
      mkdir -p $out/var/cache/nginx
    '';
We can say the contents of the runCommand argument are run imperatively, sure. (Especially: if you change the order of the bash commands, you might get a different result). But unlike the Dockerfile, the order where we declare this nginxVar package doesn't matter.

Or, say: the `copyToRoot` in the `nix2container.buildImage` takes in a list of packages where the contents are copied to root. The copying is an action; but the list of what to copy is not an action. -- And again, `copyToRoot` could be put after the `config` attribute.

The mechanisms describing how the copying is done is elsewhere.


One of the biggest ones is that that the nix2container definition is evaluated in the context of a flake.nix file that specifies all the inputs, and a `flake.lock` that guarantees they stay frozen.

By comparison, "FROM nginx" is just grabbing whatever is the latest in some external registry that you don't control— it's the same as starting a Dockerfile with "apt update; apt dist-upgrade", you have this huge chunk of external mutable state that you're dependent on which immediately throws any kind of real reproducibility out the window.

(And yes, doing "FROM nginx:x.y" or "FROM nginx:<sha>" does help a little, but the point remains that you're pulling a big binary blob that is essentially mystery meat— trying to make sense of what's in there is why there's now entire companies dedicated to untangling software bills of materials.)


Good point on the mutability of docker tags. But not sure how applicable "you're pulling a big binary blob that is essentially mystery meat" is when cache.nixos.org exists.


Fair, I suppose— both are remote build systems that you have to put trust in when you pull their tarballs.

But even in a world where Debian's reproducible build project completely achieves all its goals, a given docker build is always going to have temporal state in it if it depends on external images or a mutating package repository. So yes, you may have the Dockerfile that purportedly produced that disk image, but you're unlikely to be able to completely rebuild or verify it unless you also have a snapshot of the apt Packages.gz.

A nix2container image could in principle build completely from scratch, in just one command line invocation, with no external cache present, and get a bit-for-bit identical result. The only real "trusted" input that you have to start with is I believe a small busybox binary and gcc toolchain that is the initial bootstrap.


What if I told you that “imperative” and “declarative” are subjective terms and a matter of opinion


Every program is written in functional style if you squint hard enough.


But it seems to be doing it imperatively. I’d expect something like ‘nginxConf = pkgs.file “nginx.conf”, “contents”’ instead of ‘nginxConf = pkgs.writeText “nginx.conf”, “contents”’.

Not saying the system doesn’t apply this declaratively, but I find it difficult to intuit the above is checking for a state and applying changes only if necessary.


One distinction in Nix vs Docker is that Nix has a dag structure as opposed to a singlely linked list structure of layers.

The "writeText" function produces a derivation (basically an atomic build recipe) that produces that file. The crux of nix is that you make deterministic derivations, and then you can always refer to the results of a derivation from the hash of the derivation and its inputs.

What nix adds is glue logic to chain these derivations together in a way that preserves reproducibility of the individual imperative, but deterministic, components.

Unless you are using something like recursive-nix, you can completely evaluate the nix expression without building any of the derivations.


Also relevant to note that although Nix builds individual derivations imperatively (call this compiler, write this file, rename this directory), it completely controls all the inputs to that imperative process.

This is fundamentally different from a Dockerfile or Ansible script which have no idea what the "starting point" of the target environment is and are pretty much just mindlessly imposing mutations on top of whatever happens to already be there.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: