├── .gitignore ├── README.md ├── app ├── LICENSE ├── app.cabal ├── app.hs ├── default.nix ├── derivation.nix └── shell.nix ├── build-image.sh ├── configuration.nix ├── create-droplet.sh ├── default.nix ├── deploy.sh ├── docker.nix ├── import-image.sh ├── modules ├── app.nix ├── cron.nix ├── nginx.nix ├── ssmtp.nix └── vhost.nix ├── runvm.sh ├── site ├── app.md ├── credentials.md ├── cron.md ├── default.nix ├── deploying.md ├── docker.md ├── image.md ├── index.md ├── intro │ ├── example-0.md │ ├── example-0.nix │ ├── example-1.nix │ ├── expressions.md │ ├── index.md │ └── more-expressions.md ├── letsencrypt.md ├── runvm.md ├── site.md ├── ssmtp.md └── toplevel.md ├── upload-image.sh └── vm-nogui.nix /.gitignore: -------------------------------------------------------------------------------- 1 | .envrc 2 | s3-config 3 | .*.swp 4 | secrets/ssmtp-authpass 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nix notes 2 | 3 | This is a collection of short notes about Nix and NixOS. Almost each note 4 | corresponds to a page, to a Git commit, and to a Nix attribute that can be 5 | built. 6 | 7 | The result is a virtual machine image that actually runs as 8 | [noteed.com](https://noteed.com). 9 | 10 | 11 | ## How to read these notes 12 | 13 | The notes should be read with a copy of this repository. To make each note 14 | correspond to a commit, this repository is heavily rebased. When studying a 15 | particular note, especially the first ones, it is best to checkout the 16 | corresponding commit to see only the relevant files for that note. 17 | 18 | In other words, running `git log --patch --reverse` should be a readable 19 | tutorial about Nix and NixOS. Indeed, the following table of content is 20 | generated with `git log --reverse`! 21 | 22 | If you have never used Nix before, you may want to start with the [Introduction 23 | to Nix](site/intro/index.md), although starting directly below should be more 24 | fun. 25 | 26 | 27 | ## Bleeding edge 28 | 29 | Note: building Digital Ocean images is a recent addition to nixpkgs, and 30 | uploading a custom image is a recent feature of `doctl`. Assuming `../nixpkgs` 31 | is a recent checkout (around 2019-12-13), I'm using a Nix shell like this: 32 | 33 | ``` 34 | $ NIX_PATH=nixpkgs=../nixpkgs nix-shell -p doctl 35 | ``` 36 | 37 | 38 | ## Table of content 39 | 40 | 41 | ### Start here: building a Digital Ocean image 42 | 43 | In this commit, we introduce a short Nix expression to build a virtual machine 44 | image that can be run on Digital Ocean. The expression is short because all the 45 | machinery to do the heavy lifting is in nixpkgs. [More.](site/image.md) 46 | 47 | 48 | ### Aside: environment variables used for credentials 49 | 50 | In this commit, we talk about two files that are in fact non under version-control. 51 | They are used to store credentials for two command-line tools: `s3cmd` and `doctl`. 52 | [More.](site/credentials.md) 53 | 54 | 55 | ### Deploying the image to Digital Ocean 56 | 57 | In this commit, we add two scripts using the `s3cmd` and `doctl` tools. They 58 | use the credentials introduced in the previous commit: `s3cmd` uploads to S3 59 | the image built in the commit before, and `doctl` is used to import the image 60 | into Digital Ocean then spin a new virtual machine. [More.](site/deploying.md) 61 | 62 | 63 | ### Adding a static site to the Nginx configuration 64 | 65 | In this commit, we add a simple static site to our image. In the next commit, 66 | we'll see how to update an existing virtual machine with this static site, 67 | without rebuilding an image or a droplet. [More.](site/site.md) 68 | 69 | 70 | ### Toplevel: updating a running virtual machine 71 | 72 | In this commit, we add a new `toplevel` attribute, and a `deploy.sh` script. 73 | The toplevel corresponds to the content of a virtual machine (before it is 74 | packaged as such), and the `deploy.sh` script syncs an existing NixOS machine 75 | so that it matches the new toplevel. [More.](site/toplevel.md) 76 | 77 | 78 | ### Running the configuration inside a local VM 79 | 80 | [More.](site/runvm.md) 81 | 82 | 83 | ### Using cron to run scheduled jobs 84 | 85 | In this commit, we add a very simple system crontab demonstrating how to run a 86 | simple command every five minutes. [More.](site/cron.md) 87 | 88 | 89 | ### A simple Servant-based HTTP backend service 90 | 91 | In this commit, we add a simple Haskell application using Servant. It listens 92 | on port 8000 and we modify the Nginx configuration to forward requests to it. 93 | This also shows how to register the application as a Systemd unit. 94 | [More.](site/app.md) 95 | 96 | 97 | ### Packaging our application as a Docker image 98 | 99 | Building Docker images is a useful way to start using Nix where Docker is 100 | already established. In this commit, we show how to build a Docker image 101 | containing the application we have created in the previous commit. [More.](site/docker.md) 102 | 103 | 104 | ### Introduction to Nix 105 | 106 | In these commits, we add a few examples to learn Nix. 107 | [More.](site/intro/index.md) 108 | 109 | 110 | ### Adding automatic Let's encrypt certificate 111 | 112 | In this commit, we use the built-in support for Let's encrypt offered in the 113 | Nginx module. [More.](site/letsencrypt.md) 114 | 115 | 116 | ## Details 117 | 118 | The `configuration.nix` file is taken from the 119 | [nixos-generators](https://github.com/nix-community/nixos-generators) project, 120 | which makes it easy to build images in various formats, including the one used 121 | here. 122 | 123 | I use the same Digital Ocean facility for both S3 and the droplets (i.e. ams3). 124 | -------------------------------------------------------------------------------- /app/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Vo Minh Thu, Hypered SPRL. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE AUTHORS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION). 22 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 23 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 24 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 25 | SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /app/app.cabal: -------------------------------------------------------------------------------- 1 | name: app 2 | version: 0.0.0 3 | Cabal-Version: >= 1.8 4 | synopsis: Example Servant app 5 | description: This is a example Servant application for nix-notes. 6 | category: System 7 | license: BSD2 8 | license-file: LICENSE 9 | author: Vo Minh Thu 10 | maintainer: thu@hypered.io 11 | build-type: Simple 12 | 13 | executable app 14 | hs-source-dirs: ./ 15 | main-is: app.hs 16 | build-depends: 17 | base == 4.*, 18 | servant-server, 19 | warp 20 | ghc-options: 21 | -Wall 22 | -------------------------------------------------------------------------------- /app/app.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE TypeOperators #-} 4 | 5 | module Main where 6 | 7 | import Network.Wai.Handler.Warp 8 | import Servant 9 | 10 | 11 | ------------------------------------------------------------------------------ 12 | main :: IO () 13 | main = runSettings appSettings (serve server handler) 14 | where 15 | appSettings = (setPort 8000 . setHost "0.0.0.0") defaultSettings 16 | 17 | 18 | ------------------------------------------------------------------------------ 19 | type AddAPI = 20 | "add" :> Capture "a" Integer :> Capture "b" Integer :> Get '[JSON] Integer 21 | 22 | ------------------------------------------------------------------------------ 23 | server :: Proxy AddAPI 24 | server = Proxy 25 | 26 | handler :: Server AddAPI 27 | handler a b = pure (a + b) 28 | -------------------------------------------------------------------------------- /app/default.nix: -------------------------------------------------------------------------------- 1 | let 2 | pkgs = import {}; 3 | callPackage = pkgs.lib.callPackageWith pkgs.haskell.packages.ghc865; 4 | 5 | f = import ./derivation.nix; 6 | in 7 | callPackage f { stdenv = pkgs.stdenv; } 8 | -------------------------------------------------------------------------------- /app/derivation.nix: -------------------------------------------------------------------------------- 1 | { mkDerivation, base, servant-server, stdenv, warp }: 2 | mkDerivation { 3 | pname = "app"; 4 | version = "0.0.0"; 5 | src = ./.; 6 | isLibrary = false; 7 | isExecutable = true; 8 | executableHaskellDepends = [ base servant-server warp ]; 9 | description = "Example Servant app"; 10 | license = stdenv.lib.licenses.bsd2; 11 | } 12 | -------------------------------------------------------------------------------- /app/shell.nix: -------------------------------------------------------------------------------- 1 | let 2 | pkgs = import {}; 3 | callPackage = pkgs.lib.callPackageWith pkgs.haskell.packages.ghc843; 4 | 5 | f = import ./derivation.nix; 6 | drv = callPackage f { stdenv = pkgs.stdenv; }; 7 | in 8 | 9 | pkgs.stdenv.mkDerivation { 10 | name = "app-env"; 11 | buildInputs = [ 12 | ] ++ drv.env.nativeBuildInputs; 13 | } 14 | -------------------------------------------------------------------------------- /build-image.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Build a virtual machine image suitable for Digital Ocean. The resulting file 4 | # will be result/nixos.qcow2.gz. 5 | # 6 | # Note that we need a recent nixpkgs Git repository that knows about the 7 | # digitalOceanImage attribute. 8 | 9 | nix-build -A image 10 | -------------------------------------------------------------------------------- /configuration.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, 2 | nix-notes-version, 3 | ... }: 4 | { 5 | services.sshd.enable = true; 6 | 7 | networking.firewall.allowedTCPPorts = [ 80 443 ]; 8 | 9 | users.users.root.password = "nixos"; 10 | services.openssh.permitRootLogin = lib.mkDefault "yes"; 11 | services.mingetty.autologinUser = lib.mkDefault "root"; 12 | 13 | imports = [ 14 | modules/app.nix 15 | modules/cron.nix 16 | modules/nginx.nix 17 | modules/ssmtp.nix 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /create-droplet.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Note that the image name is actually an image ID (returned after the image 4 | # has been imported), and not the image name chosen at import time. 5 | 6 | doctl compute droplet create nix-notes-1 \ 7 | --region ams3 \ 8 | --image 56382035 \ 9 | --size s-1vcpu-1gb \ 10 | --ssh-keys 98:9a:9b:21:66:1e:b2:7f:35:58:d7:ea:ca:3e:64:bd \ 11 | --wait 12 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { nixpkgs ? 2 | , system ? builtins.currentSystem 3 | , nix-notes-version ? "not-given" 4 | }: 5 | let 6 | pkgs = import nixpkgs {}; 7 | in 8 | rec { 9 | os = import "${toString nixpkgs}/nixos/lib/eval-config.nix" { 10 | inherit system; 11 | extraArgs = { inherit nix-notes-version; }; 12 | modules = [ 13 | ./configuration.nix 14 | "${toString nixpkgs}/nixos/modules/virtualisation/digital-ocean-image.nix" 15 | ]; 16 | }; 17 | 18 | qemu = import "${toString nixpkgs}/nixos/lib/eval-config.nix" { 19 | inherit system; 20 | extraArgs = { inherit nix-notes-version; }; 21 | modules = [ 22 | ./configuration.nix 23 | "${toString nixpkgs}/nixos/modules/virtualisation/qemu-vm.nix" 24 | ./vm-nogui.nix 25 | ]; 26 | }; 27 | 28 | # Build with nix-build -A 29 | image = os.config.system.build.digitalOceanImage; 30 | toplevel = os.config.system.build.toplevel; 31 | app = import ./app; 32 | site = (import ./vhost.nix { inherit pkgs nix-notes-version; }).site; 33 | crontab = os.config.environment.etc.crontab; 34 | runvm = qemu.config.system.build.vm; 35 | docker = import ./docker.nix; 36 | } 37 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # This script builds the new system, uploads it and activates it on the target 4 | # system. This is similar to the following blog post: 5 | # https://vaibhavsagar.com/blog/2019/08/22/industrial-strength-deployments/. 6 | 7 | set -euo pipefail 8 | 9 | TARGET="root@165.22.200.188" 10 | 11 | echo Building toplevel... 12 | GIT_DESCRIBE=$(git describe --dirty --long) 13 | PROFILE_PATH="$(nix-build --no-out-link -A toplevel \ 14 | --argstr nix-notes-version $GIT_DESCRIBE)" 15 | echo $PROFILE_PATH 16 | 17 | echo Copying toplevel closure to target system... 18 | nix-copy-closure --to --use-substitutes $TARGET $PROFILE_PATH 19 | 20 | echo Copying secrets to target system... 21 | scp secrets/ssmtp-authpass $TARGET:/run/keys/ssmtp-authpass 22 | 23 | echo Activating copied toplevel... 24 | ssh $TARGET -- \ 25 | "nix-env --profile /nix/var/nix/profiles/system --set $PROFILE_PATH && \ 26 | /nix/var/nix/profiles/system/bin/switch-to-configuration switch" 27 | -------------------------------------------------------------------------------- /docker.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | let 3 | app = import ./app; 4 | in 5 | 6 | pkgs.dockerTools.buildImage { 7 | name = "app"; 8 | contents = [ app ]; 9 | config = { 10 | Cmd = [ "${app}/bin/app" ]; 11 | ExposedPorts = { 12 | "8000/tcp" = {}; 13 | }; 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /import-image.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Import an image into Digital Ocean from an URL. The URL here is from a S3 4 | # bucket where we have uploaded the image. 5 | 6 | doctl compute image create nix-notes \ 7 | --region ams3 \ 8 | --image-url https://hypered.ams3.digitaloceanspaces.com/nixos.qcow2.gz \ 9 | --image-description 'Example image from my nix-notes' 10 | -------------------------------------------------------------------------------- /modules/app.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, 2 | nix-notes-version, 3 | ... }: 4 | { 5 | systemd.services.app = { 6 | wantedBy = [ "multi-user.target" ]; 7 | script = "${import ../app}/bin/app"; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /modules/cron.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, 2 | nix-notes-version, 3 | ... }: 4 | { 5 | services.cron = { 6 | enable = true; 7 | systemCronJobs = [ 8 | "*/5 * * * * root date >> /tmp/cron.log" 9 | ]; 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /modules/nginx.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, 2 | nix-notes-version, 3 | ... }: 4 | let 5 | gitcraft = pkgs.fetchFromGitHub { 6 | owner = "noteed"; 7 | repo = "gitcraft"; 8 | rev = "96bc18ade14dca67da208b0a739f955f611cc2d5"; 9 | sha256 = "17lggp9kf4j288zi71pp0rqbrjzhaqx56x4hbzx8hn04m982riag"; 10 | }; 11 | git-notes = pkgs.fetchFromGitHub { 12 | owner = "noteed"; 13 | repo = "git-notes"; 14 | rev = "3475e6532ec9c967584183fec7d9aafb2151bd50"; 15 | sha256 = "1mx297l6q353934p1rqp8yc60yjhzd6kca5pjwv9m3hq18gbny7r"; 16 | }; 17 | noteed-github-com = pkgs.fetchFromGitHub { 18 | owner = "noteed"; 19 | repo = "noteed.github.com"; 20 | rev = "75fa1e634e8b4409910d83c35ddb245349985713"; 21 | sha256 = "1yfkwzrchc8dhzzi8pxx7rsk8l72hv84p6n40rxam5hm8x5pn22p"; 22 | }; 23 | 24 | in 25 | { 26 | services.nginx = { 27 | enable = true; 28 | virtualHosts."noteed.com" = { 29 | forceSSL = true; 30 | enableACME = true; 31 | inherit (import ./vhost.nix { inherit pkgs nix-notes-version; }) locations; 32 | }; 33 | }; 34 | 35 | security.acme.acceptTerms = true; 36 | security.acme.certs = { 37 | "noteed.com".email = "noteed@gmail.com"; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /modules/ssmtp.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, 2 | nix-notes-version, 3 | ... }: 4 | { 5 | services.ssmtp = { 6 | enable = true; 7 | hostName = "smtp.fastmail.com:465"; 8 | domain = "noteed.com"; 9 | useTLS = true; 10 | authUser = "thu@fastmail.com"; 11 | authPassFile = "/run/keys/ssmtp-authpass"; 12 | setSendmail = true; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /modules/vhost.nix: -------------------------------------------------------------------------------- 1 | { pkgs, 2 | nix-notes-version, 3 | ... }: 4 | let 5 | gitcraft-version = "96bc18ade14dca67da208b0a739f955f611cc2d5"; 6 | git-notes-version = "3475e6532ec9c967584183fec7d9aafb2151bd50"; 7 | noteed-github-com-version = "75fa1e634e8b4409910d83c35ddb245349985713"; 8 | gitcraft = pkgs.fetchFromGitHub { 9 | owner = "noteed"; 10 | repo = "gitcraft"; 11 | rev = gitcraft-version; 12 | sha256 = "17lggp9kf4j288zi71pp0rqbrjzhaqx56x4hbzx8hn04m982riag"; 13 | }; 14 | git-notes = pkgs.fetchFromGitHub { 15 | owner = "noteed"; 16 | repo = "git-notes"; 17 | rev = git-notes-version; 18 | sha256 = "1mx297l6q353934p1rqp8yc60yjhzd6kca5pjwv9m3hq18gbny7r"; 19 | }; 20 | noteed-github-com = pkgs.fetchFromGitHub { 21 | owner = "noteed"; 22 | repo = "noteed.github.com"; 23 | rev = noteed-github-com-version; 24 | sha256 = "1yfkwzrchc8dhzzi8pxx7rsk8l72hv84p6n40rxam5hm8x5pn22p"; 25 | }; 26 | nix-notes = import ../site { 27 | inherit 28 | nix-notes-version gitcraft-version git-notes-version 29 | noteed-github-com-version; 30 | }; 31 | 32 | in 33 | { 34 | site = nix-notes.html.all; 35 | locations = { 36 | "/add".proxyPass = "http://127.0.0.1:8000"; 37 | "/gitcraft/".alias = (import gitcraft {}).html.all + "/"; 38 | "/git-notes/".alias = (import git-notes).site + "/"; 39 | "/nix-notes/".alias = nix-notes.html.all + "/"; 40 | "~ ^/version$" = { 41 | alias = nix-notes.html.version; 42 | extraConfig = ''default_type "text/html";''; 43 | }; 44 | "/".alias = (import noteed-github-com {}).html.all + "/"; 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /runvm.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Type Ctrl-a x to exit QEMU. 4 | 5 | set -e 6 | 7 | nix-build -A runvm 8 | ./result/bin/run-nixos-vm 9 | -------------------------------------------------------------------------------- /site/app.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Nix notes 3 | footer: © Võ Minh Thu, 2019. 4 | --- 5 | 6 | 7 | ### A simple Servant-based HTTP backend service 8 | 9 | ``` 10 | nix-build -A app 11 | @result@ 12 | ``` 13 | 14 | It is possible to evaluate our configuration to see the generated Systemd unit 15 | file: 16 | 17 | ``` 18 | $ nix-instantiate --eval -A 'os.config.systemd.units."app.service".text' | jq -r 19 | ``` 20 | -------------------------------------------------------------------------------- /site/credentials.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Nix notes 3 | footer: © Võ Minh Thu, 2019. 4 | --- 5 | 6 | 7 | ## Aside: environment variables used for credentials 8 | 9 | As can be seen in the `.gitignore` file, I'm using two files that are not versioned: 10 | `.envrc` and `s3-config`. 11 | 12 | The `.envrc` file is used by an utility called `direnv` and contains 13 | environment variable definitions. In particular for this project, it contains 14 | credentials for both an S3 API, and the Digital Ocean API. Those values are 15 | used by `doctl` and `s3cmd`. 16 | 17 | Although I'm using Digital Ocean Spaces instead of Amazon S3, the environment 18 | variables are still named `AWS_*`. 19 | 20 | Upon entering a directory with an `.envrc` file, `direnv` will load the 21 | environment variables it defines: 22 | 23 | ``` 24 | $ cd nix-notes 25 | /home/thu/projects/nix-notes 26 | direnv: loading .envrc 27 | direnv: export +AWS_ACCESS_KEY_ID +AWS_SECRET_ACCESS_KEY +DIGITALOCEAN_ACCESS_TOKEN 28 | ``` 29 | 30 | In addition to the credentials, the `s3-config` file is used to configure 31 | `s3cmd` to point to Spaces instead of defaulting to Amazon. That part of the 32 | file is not sensitive but it may happen that credentials get written to it. 33 | 34 | ``` 35 | $ grep host_b s3-config 36 | host_base = ams3.digitaloceanspaces.com 37 | host_bucket = %(bucket)s.ams3.digitaloceanspaces.com 38 | ``` 39 | -------------------------------------------------------------------------------- /site/cron.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Nix notes 3 | footer: © Võ Minh Thu, 2019. 4 | --- 5 | 6 | 7 | ### Using cron to run scheduled jobs 8 | 9 | ``` 10 | nix-build -A crontab 11 | @result@ 12 | ``` 13 | 14 | The above command builds the file that will end up at `/etc/crontab` within the 15 | virtual machine. 16 | 17 | Note: the ability to describe files in the `/etc` hierarchy is a special 18 | feature of NixOS; it hasn't support to create arbitrary files elsewhere. 19 | 20 | It is possible to evaluate our configuration to see the generated Systemd unit 21 | file: 22 | 23 | ``` 24 | $ nix-instantiate --eval -A 'os.config.systemd.units."cron.service".text' | jq -r 25 | ``` 26 | -------------------------------------------------------------------------------- /site/default.nix: -------------------------------------------------------------------------------- 1 | { nixpkgs ? 2 | , nix-notes-version ? "not-given" 3 | , gitcraft-version ? "not-given" 4 | , git-notes-version ? "not-given" 5 | , noteed-github-com-version ? "not-given" 6 | }: 7 | 8 | let 9 | pkgs = import nixpkgs {}; 10 | 11 | design-system-version = "e1fe8d82349f4a084dee751a9c4bc5ef81ee68bb"; 12 | design-system = pkgs.fetchFromGitHub { 13 | owner = "hypered"; 14 | repo = "design-system"; 15 | rev = design-system-version; 16 | sha256 = "1lqsx2zq2ymib9x4b0xncgx4wjw1mkphr4zda84fj4lbx445rdii"; 17 | }; 18 | inherit (import design-system {}) to-html replace-md-links; 19 | 20 | in rec 21 | { 22 | md.index = ./index.md; 23 | md.image = ./image.md; 24 | md.deploying = ./deploying.md; 25 | md.version = pkgs.writeText "version.md" '' 26 | --- 27 | title: Versions 28 | footer: © Võ Minh Thu, 2019. Version ${nix-notes-version}. 29 | --- 30 | 31 | 32 | ---------------------------------------------------------------- ------------------------ 33 | [design-system](https://github.com/hypered/design-system) ${design-system-version} 34 | [gitcraft](https://github.com/noteed/gitcraft) ${gitcraft-version} 35 | [git-notes](https://github.com/noteed/git-notes) ${git-notes-version} 36 | [nix-notes](https://github.com/noteed/nix-notes) ${nix-notes-version} 37 | [noteed.github.com](https://github.com/noteed/noteed.github.com) ${noteed-github-com-version} 38 | ---------------------------------------------------------------- ------------------------ 39 | ''; 40 | 41 | html.index = to-html md.index; 42 | html.image = to-html md.image; 43 | html.deploying = to-html md.deploying; 44 | html.version = to-html md.version; 45 | html.all = pkgs.runCommand "all" {} '' 46 | mkdir $out 47 | cp ${html.index} $out/index.html 48 | cp ${html.image} $out/image.html 49 | cp ${html.deploying} $out/deploying.html 50 | cp ${html.version} $out/version.html 51 | ${pkgs.bash}/bin/bash ${replace-md-links} $out 52 | ''; 53 | } 54 | -------------------------------------------------------------------------------- /site/deploying.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Nix notes 3 | footer: © Võ Minh Thu, 2019. 4 | --- 5 | 6 | 7 | ## Deploying the image to Digital Ocean 8 | 9 | The first step is to build a NixOS [image](image.md) in the qcow2 format: 10 | 11 | ``` 12 | $ ./build-image.sh 13 | ``` 14 | 15 | The second step is to upload the image somewhere where Digital Ocean can later 16 | import it using HTTP. Here we upload the image to S3: 17 | 18 | ``` 19 | $ ./upload-image.sh 20 | ``` 21 | 22 | The third step is to import the image into Digital Ocean, so we can refer to it 23 | when creating a new droplet: 24 | 25 | ``` 26 | $ ./import-image.sh 27 | ``` 28 | 29 | Finally, the fourth step is create a new droplet, specifying the custom image: 30 | 31 | ``` 32 | $ ./create-droplet.sh 33 | ``` 34 | 35 | TODO The image ID is currently hard-coded in the `create-droplet.sh` script. 36 | -------------------------------------------------------------------------------- /site/docker.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Nix notes 3 | footer: © Võ Minh Thu, 2019. 4 | --- 5 | 6 | 7 | ## Packaging our application as a Docker image 8 | 9 | ``` 10 | $ nix-build -A docker 11 | @result@ 12 | ``` 13 | -------------------------------------------------------------------------------- /site/image.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Nix notes 3 | footer: © Võ Minh Thu, 2019. 4 | --- 5 | 6 | 7 | ## Start here: building a Digital Ocean image 8 | 9 | ``` 10 | $ nix-build -A image 11 | @result@ 12 | ``` 13 | 14 | This derivation builds a virtual machine image suitable for Digital Ocean: one 15 | can import the resulting file as a custom image then select that image when 16 | creating a new droplet (a droplet is the name DO gives to a virtual machine). 17 | We'll do that in two commits (i.e. [here](deploying.md)). 18 | 19 | As can be seen in `default.nix`, the Nix expression detailing our complete 20 | image is quite simple: we call `/nixos/lib/eval-config.nix`, passing 21 | two modules: one defined in nixpkgs containing the DO-specific bits, and 22 | `./configuration.nix`. 23 | 24 | Really, that's nixpkgs doing all the work! 25 | 26 | Finally our `./configuration.nix` is a standard NixOS configuration file, just 27 | like the one you would have in `/etc/nixos/configuration.nix`. It imports 28 | `modules/nginx.nix` which could alternatively be inlined in `configuration.nix` 29 | since both are short, but doing so will be useful when we add more to our 30 | configuration. 31 | -------------------------------------------------------------------------------- /site/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Nix notes 3 | --- 4 | 5 | 6 | ## About these pages 7 | 8 | ``` 9 | $ nix-build -A site 10 | @result@ 11 | ``` 12 | 13 | These pages are served by NixOS, running on a Digital Ocean virtual machine. 14 | They describe how you can build the same virtual machine image and how to run 15 | it. 16 | 17 | All you need is a copy of the following repository: 18 | [github.com/noteed/nix-notes](https://github.com/noteed/nix-notes). 19 | 20 | By running the above command, you'll build these pages. By running the command 21 | of the [next page](image.md), you'll build the complete virtual machine image. 22 | -------------------------------------------------------------------------------- /site/intro/example-0.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Nix notes 3 | footer: © Võ Minh Thu, 2019. 4 | --- 5 | 6 | 7 | ## Minimal examples 8 | 9 | The file `example-0.nix`, called a derivation, has the following content: 10 | 11 | ``` 12 | let 13 | pkgs = import {}; 14 | 15 | in 16 | pkgs.runCommand "example-0" {} '' 17 | echo 'Built with Nix.' > $out 18 | '' 19 | ``` 20 | 21 | It is a script that, when built with Nix, creates a file with the content 22 | `"Build with Nix."`. 23 | 24 | ``` 25 | $ nix build --file ./example-0.nix 26 | [1 built, 0.0 MiB DL] 27 | $ cat result 28 | Built with Nix. 29 | ``` 30 | 31 | The file `result` is a symlink to the actual result, located in the Nix store: 32 | 33 | ``` 34 | $ readlink result 35 | /nix/store/9f96vvg87vj6fn8ck37xz5annaiwmnp1-example-0 36 | ``` 37 | 38 | 39 | ## Multiple results 40 | 41 | Multiple results can be created by a single Nix script. Here is the content of 42 | `example-1.nix`: 43 | 44 | ``` 45 | let 46 | pkgs = import {}; 47 | 48 | in 49 | { 50 | example-0 = pkgs.runCommand "example-0" {} '' 51 | echo 'Built with Nix.' > $out 52 | ''; 53 | example-1 = pkgs.runCommand "example-1" {} '' 54 | echo 'Built with Nix too.' > $out 55 | ''; 56 | } 57 | ``` 58 | 59 | When building that script, two results are created: `result` and `result-1`. 60 | One can also choose to build a particular attribute: 61 | 62 | ``` 63 | $ nix build --file site/intro/example-1.nix example-1 64 | ``` 65 | -------------------------------------------------------------------------------- /site/intro/example-0.nix: -------------------------------------------------------------------------------- 1 | let 2 | pkgs = import {}; 3 | 4 | in 5 | pkgs.runCommand "example-0" {} '' 6 | echo 'Built with Nix.' > $out 7 | '' 8 | -------------------------------------------------------------------------------- /site/intro/example-1.nix: -------------------------------------------------------------------------------- 1 | let 2 | pkgs = import {}; 3 | 4 | in 5 | { 6 | example-0 = pkgs.runCommand "example-0" {} '' 7 | echo 'Built with Nix.' > $out 8 | ''; 9 | example-1 = pkgs.runCommand "example-1" {} '' 10 | echo 'Built with Nix too.' > $out 11 | ''; 12 | } 13 | -------------------------------------------------------------------------------- /site/intro/expressions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Nix notes 3 | footer: © Võ Minh Thu, 2019. 4 | --- 5 | 6 | 7 | ## Evaluating expressions 8 | 9 | The name "Nix" is also the name of the language used to describe Nix packages. 10 | To play with the language we can use a few approaches. 11 | 12 | We can use `nix eval` or `nix-instantiate` to directly evaluate an expression: 13 | 14 | ``` 15 | $ nix eval '(1 + 2)' 16 | 3 17 | ``` 18 | 19 | ``` 20 | $ nix-instantiate --eval --expr '1 + 2' 21 | 3 22 | ``` 23 | 24 | We can also use them to evaluate an expression written to a file: 25 | 26 | ``` 27 | $ echo '{ a = 1 + 2; }' > expression.nix 28 | $ nix eval -f expression.nix a 29 | 3 30 | ``` 31 | 32 | ``` 33 | $ echo '1 + 2' > expression.nix 34 | $ nix-instantiate --eval expression.nix 35 | 3 36 | ``` 37 | 38 | We can use the interactive REPL: 39 | 40 | ``` 41 | $ nix repl 42 | Welcome to Nix version 2.1.3. Type :? for help. 43 | 44 | nix-repl> 1 + 2 45 | 3 46 | 47 | nix-repl> ^D 48 | ``` 49 | 50 | What if we try to _build_ an expression that doesn't describe a package ? 51 | 52 | ``` 53 | $ nix build -f expression.nix a 54 | error: expression does not evaluate to a derivation (or a set or list of those) 55 | ``` 56 | 57 | And what happens if we evaluate an derivation ? 58 | -------------------------------------------------------------------------------- /site/intro/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Nix notes 3 | footer: © Võ Minh Thu, 2019. 4 | --- 5 | 6 | 7 | ## Introduction to Nix 8 | 9 | - [Minimal examples](example-0.md) 10 | - [Evaluating expressions](expressions.md) 11 | - [More expressions](more-expressions.md) 12 | -------------------------------------------------------------------------------- /site/intro/more-expressions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Nix notes 3 | footer: © Võ Minh Thu, 2019. 4 | --- 5 | 6 | 7 | ## More expressions 8 | 9 | Lambdas: 10 | 11 | ``` 12 | $ nix eval expr '(a: a + 1)' 13 | 14 | ``` 15 | 16 | ``` 17 | $ nix eval expr '(a: a + 1) 2' 18 | 3 19 | ``` 20 | 21 | Attribute sets: 22 | 23 | ``` 24 | $ nix eval '({ a = 1; })' 25 | { a = 1; } 26 | ``` 27 | 28 | ``` 29 | $ nix eval '({ a = 1; }.a)' 30 | 1 31 | ``` 32 | 33 | Understanding the nested keys syntactic sugar: 34 | 35 | ``` 36 | $ nix-instantiate --eval --expr '{ a.b.c = 1; }.a.b' 37 | { c = 1; } 38 | $ nix-instantiate --eval --expr '{ a.b.c = 1; }' 39 | { a = ; } 40 | $ nix-instantiate --eval --expr --strict '{ a.b.c = 1; }' 41 | { a = { b = { c = 1; }; }; } 42 | ``` 43 | 44 | Understanding the `` path notation: 45 | 46 | ``` 47 | $ pwd 48 | /tmp 49 | $ cat a.nix 50 | { a = 1 ; } 51 | $ nix-instantiate --eval a.nix 52 | { a = 1; } 53 | $ nix-instantiate --eval -I mypkgs=./a.nix --expr '' 54 | /tmp/a.nix 55 | $ nix-instantiate --eval -I mypkgs=./a.nix --expr 'import ' 56 | { a = 1; } 57 | $ nix-instantiate --eval -I mypkgs=./a.nix --expr 'with import ; a' 58 | 1 59 | ``` 60 | 61 | ``` 62 | $ nix-instantiate --eval --expr --strict '' 63 | /home/thu/.nix-defexpr/channels/nixpkgs 64 | ``` 65 | -------------------------------------------------------------------------------- /site/letsencrypt.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Nix notes 3 | footer: © Võ Minh Thu, 2019. 4 | --- 5 | 6 | 7 | ## Adding automatic Let's encrypt certificate 8 | -------------------------------------------------------------------------------- /site/runvm.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Nix notes 3 | footer: © Võ Minh Thu, 2019. 4 | --- 5 | 6 | 7 | ## Running the configuration inside a local VM 8 | 9 | ``` 10 | nix-build -A runvm 11 | @result@ 12 | ``` 13 | 14 | This derivation creates a script to launch a QEMU virtual machine using our 15 | configuration, but sharing the Nix store with the host system. This is handy to 16 | try the configuration before creating an image or uploading its toplevel to a 17 | target machine. 18 | 19 | Simply run the created script. You will be logged in automatically as root, and 20 | can try for instance to get a page from Nginx. 21 | 22 | ``` 23 | $ result/bin/run-nixos-vm 24 | ... 25 | # curl 127.0.0.1/git-notes/init.html 26 | ... 27 | ``` 28 | 29 | Type `Ctrl-a x` to exit QEMU. 30 | -------------------------------------------------------------------------------- /site/site.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Nix notes 3 | footer: © Võ Minh Thu, 2019. 4 | --- 5 | 6 | 7 | ## Adding a static site to the Nginx configuration 8 | -------------------------------------------------------------------------------- /site/ssmtp.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Nix notes 3 | footer: © Võ Minh Thu, 2019. 4 | --- 5 | 6 | 7 | ## Sending emails with ssmtp 8 | 9 | To allow the virtual machine to send emails, we enable `services.ssmtp` in the 10 | `configuration.nix`, and fill in some additional details. 11 | 12 | The most interesting one is `authPassFile = "/run/keys/ssmtp-authpass"`. It 13 | contains the path to a file containing the password to use an SMTP server. That 14 | file should not be versioned (as can be see in `.gitignore`) and should not be 15 | in the Nix store. The `deploy.sh` script is adapted to copy secrets outside the 16 | Nix store before activating the new configuration. 17 | 18 | Testing if emails can ben sent can be done by SSHing into the remote host (or 19 | using the `runvm.sh` script) with the following command: 20 | 21 | ``` 22 | echo -e -n "To: Thu\nFrom: noteed.com\nSubject: Test\n\nTesting ssmtp" \ 23 | | sendmail -v noteed@gmail.com 24 | ``` 25 | 26 | Note: if you don't include a "From" field, the one used is from `/etc/passwd`, 27 | which is "System administrator" for the root user. 28 | -------------------------------------------------------------------------------- /site/toplevel.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Nix notes 3 | footer: © Võ Minh Thu, 2019. 4 | --- 5 | 6 | 7 | ## Toplevel: updating a running virtual machine 8 | 9 | ``` 10 | nix-build -A toplevel 11 | @result@ 12 | ``` 13 | 14 | In the blog post [Industrial-strength Deployments in Three 15 | Commands](https://vaibhavsagar.com/blog/2019/08/22/industrial-strength-deployments/), 16 | the toplevel is defined as 17 | 18 | ``` 19 | let 20 | nixos = import { 21 | configuration = import ./configuration.nix; 22 | }; 23 | in 24 | nixos.system 25 | ``` 26 | 27 | and is described as being the whole system. To be specific, the closure of 28 | toplevel is a complete system and can be packaged as a rootfs, or in our case 29 | as a qcow2 virtual machine [image](image.md). It also contains a script to 30 | "activate" the system, for instance by copying files into `/etc/`. 31 | 32 | To update a remote NixOS machine, the toplevel closure can be uploaded to it, 33 | then activated there. See the above blog post for a description. 34 | 35 | Say for instance that we update `site/index.html`, then we can update an 36 | existing droplet with: 37 | 38 | ``` 39 | $ ./deploy.sh 40 | ``` 41 | 42 | instead of re-building a complete virtual machine image and provisioning a new 43 | droplet. 44 | 45 | TODO The droplet IP address is currently hard-coded in the `deploy.sh` script. 46 | 47 | TODO Elaborate if I should copy the target `/etc/nixos/` configuration. I think 48 | I check and that it only imports the user-data and the file from nixpkgs that I 49 | already have. 50 | 51 | TODO Check the links provided in the blog post for additional insights. 52 | -------------------------------------------------------------------------------- /upload-image.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Upload the image to S3 (here my configuration in the s3-config file points to 4 | # a Digital Ocean Space). The corresponding HTTPS URL will then be used to 5 | # import the image into my Digital Ocean custom images. 6 | # 7 | # "hypered" is the name of my S3 bucket. 8 | # 9 | # Note that the image is publicly accessible (although it URL should ne known), 10 | # don't put sensitive stuff in it. 11 | 12 | s3cmd -c s3-config put result/nixos.qcow2.gz s3://hypered --acl-public 13 | -------------------------------------------------------------------------------- /vm-nogui.nix: -------------------------------------------------------------------------------- 1 | # This is the vm-nogui.nix file from nixos-generators. 2 | # It might be a good idea to merge its vm.nix file here too. 3 | 4 | { pkgs, ... }: 5 | let 6 | # https://unix.stackexchange.com/questions/16578/resizable-serial-console-window 7 | resize = pkgs.writeScriptBin "resize" '' 8 | old=$(stty -g) 9 | stty raw -echo min 0 time 5 10 | printf '\033[18t' > /dev/tty 11 | IFS=';t' read -r _ rows cols _ < /dev/tty 12 | stty "$old" 13 | stty cols "$cols" rows "$rows" 14 | ''; 15 | in { 16 | virtualisation.graphics = false; 17 | virtualisation.qemu.options = [ "-serial mon:stdio" ]; 18 | 19 | environment.systemPackages = [ resize ]; 20 | environment.loginShellInit = "${resize}/bin/resize"; 21 | } 22 | --------------------------------------------------------------------------------