Skip to content

takeda/nix-cde

Repository files navigation

Nix-CDE

Nix-CDE (Nix-based Common Development Envrionemnt) provides a reproducible development environment that abstracts away Nix rough edges through the use of NixOS modules.

Motivation

Nix provides great tooling for building projects. The problem I had with it is that first I had to find those projects, then learn how to use them (which also assumed knowing well how Nix works), often tweaking things as I learned more about it. That also created a lot of boilerplate that I had to copy from project to project, and if I learned something new I had to update it everywhere. Nix-CDE's goal is to abstract all that boilerplate away.

Example

Prerequisites

  • you need to have Nix installed
  • make sure the flake feature is enabled by adding to the /etc/nix/nix.conf:
experimental-features = nix-command flakes

Create following files

# we are overriding files in later staps, but this is good starting point
nix flake init -t github:takeda/nix-cde 

Create flake.nix

{
  description = "A simple-app";

  # Nix dependencies for our flake (most of the time you won't change this)
  inputs = {
    flake-utils.url = "github:numtide/flake-utils";
    nix-cde.url = "github:takeda/nix-cde";
  };

  outputs = { self, flake-utils, nix-cde, ... }: flake-utils.lib.eachDefaultSystem (build_system:
  let
    cde = is_shell: nix-cde.lib.mkCDE ./project.nix { inherit build_system is_shell self; };
    # version of CDE that is used for building docker (forces x86_64-linux binaries)
    cde-docker = nix-cde.lib.mkCDE ./project.nix {
      inherit build_system self;
      host_system = "x86_64-linux";
    };
  in {
    # used when invoking `nix develop`
    devShells.default = (cde true).outputs.out_shell;
    # used when invoking `nix build`
    packages.default = (cde false).outputs.out_python;
    # used when invoking `nix build .#docker`
    packages.docker = cde-docker.outputs.out_docker;
  });
}

Create project.nix

{ config, modulesPath, nix2container, pkgs, ... }:

{
  # modules that our project will use
  require = [
    "${modulesPath}/languages/python-poetry.nix"
    "${modulesPath}/builders/docker-nix2container.nix"
  ];

  # name of our application
  name = "simple-app";

  # files to exclude (there often are files that you need to have in git, but
  # you don't want nix to rebuild your app if they change)
  # simpliarly to .gitignore you can also exclude everything and implicitly
  # list files you want included
  src_exclude = [''
    *
    !/simple_app.py
    !/pyproject.toml
    !/poetry.lock
  ''];

  lean_python = {
    enable = true;
    package = pkgs.python311;
    expat = true;
    zlib = true;
  };

  python = {
    enable = true;
    package = pkgs.python311; # use python3.11
    inject_app_env = true; # add project dependencies to dev shell (simplar to to being in an activated virtualenv)
    prefer_wheels = false; # whether to compile packages ourselves or use wheels
  };

  docker = {
    enable = true;
    # call /bin/hello when running the container
    command = [ "${config.out_python}/bin/hello" ];
    # place python in a separate layer
    layers = with nix2container; [
      (buildLayer { deps = [ pkgs.python311 ]; })
    ];
  };

  # packages that should be available in dev shell
  dev_commands = with pkgs; [
    dive
  ];
}

Create simple_app.py

def cli():
    print("Hello, World!")

Create pyproject.toml

[tool.poetry]
name = "simple-app"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]
packages = [
  { include = "simple_app.py" }
]

[tool.poetry.dependencies]
python = "^3.10"

[tool.poetry.dev-dependencies]

[tool.poetry.scripts]
hello = "simple_app:cli"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

Enter dev shell

Create lock file (poetry should be available even if you didn't have it installed on your computer).

$ nix develop -c poetry lock

Invoke nix develop to enter dev shell and run the command to check it works. nix develop essentially creates something similar to Python's virtualenv with your package installed in editable mode. You can make change to your program and your change will take effect immediately. No need of rebuilding or re-running nix develop.

$ nix develop
$ hello
Hello, World!

Note: It is highly encouraged to install direnv and nix-direnv. If you create .envrc file with use flake then the shell will automatically change upon entering.

Build the app

This creates a Nix package with our app and creates result symlink that points to it.

$ nix build
$ ./result/bin/hello
Hello, World!

Building a docker image with our app

$ nix run .#docker.copyToDockerDaemon
$ docker images
REPOSITORY   TAG                                IMAGE ID       CREATED        SIZE
simple-app   dm3hinmdgp5d4cgjy4x2yxy811bvdp96   a70176be91af   N/A            178MB
$ docker run --rm a70176be91af
Hello, World!

Note: Docker images typically contain linux binaries. This is the main reason why on Mac docker actually runs on a VM running linux. So while above example will work without problems on Linux machine you might run into issues if you use a Mac. To build a docker image on Mac you will need a remote Linux builder. It could be real linux machine, or you can run one through docker.

Reduce size of the docker image

If you noticed, in the above example the container was containing only our app, but it was still 138MB. This is because python is quite large. There's a way to shrink it down by excluding some dependencies that we aren't using in our application.

Here's how to do it:

  1. Modify project.nix and add "${modulesPath}/tools/mod-lean-python.nix" in the require section, like this:
 require = [
      "${modulesPath}/languages/python-poetry.nix"
      "${modulesPath}/builders/docker-nix2container.nix"
      "${modulesPath}/tools/mod-lean-python.nix"
    ];
  1. add the following block
   lean_python = {
     enable = true;
     package = pkgs.python311;
     expat = true;
     zlib = true;
   };
  1. update python.package to point to config.out_lean_python instead of pkgs.python311, like this:
  python = {
    enable = true;
    package = config.out_lean_python;
    inject_app_env = true; # add project dependencies to dev shell (simplar to to being in an activated virtualenv)
    prefer_wheels = false; # whether to compile packages ourselves or use wheels
  };
  1. update 'docker.layers' ro point to config.out_lean_python instead of pkgs.python311, like this:
  docker = {
    enable = true;
    # call /bin/hello when running the container
    command = [ "${config.out_python}/bin/hello" ];
    # place python in a separate layer
    layers = with nix2container; [
      (buildLayer { deps = [ config.out_lean_python ]; })
    ];
  };
  1. re-run build:
$ nix run .#docker.copyToDockerDaemon
# this will take a bit longer than usual, as python is being compiled
# subsequent calls should be quick due to caching
$ docker images
REPOSITORY   TAG                                IMAGE ID       CREATED        SIZE
simple-app   dm3hinmdgp5d4cgjy4x2yxy811bvdp96   a70176be91af   N/A            178MB
simple-app   slgngdkfbds8yfgbil12l04v0k6pwlhv   b503f16dd9fa   N/A            68.9MB
$ docker run --rm b503f16dd9fa
Hello, World!

As we can see, this reduced the size of the container to 69MB. It's possible that it could be reduced even further by using musl, and perhaps bash could be replaced with something else, unfortunately I don't know yet how to do that.

About

Nix Common Development Environment

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published