└── README.md /README.md: -------------------------------------------------------------------------------- 1 | # Deploying your application with NixOS 2 | 3 | - [Getting NixOS on a VPS](#getting-nixos-on-a-vps) 4 | - [The NixOS Lustrate method](#the-nixos-lustrate-method) 5 | - [Alternative ways to get NixOS on your server](#alternative-ways-to-get-nixos-on-your-server) 6 | - [A simple Haskell web service](#a-simple-haskell-web-service) 7 | - [Introducing NixOS modules](#introducing-nixos-modules) 8 | - [A quick example : MailHog mail catching facility](#a-quick-example--mailhog-mail-catching-facility) 9 | - [Packaging your app as a NixOS module](#packaging-your-app-as-a-nixos-module) 10 | - [Deploying with Morph](#deploying-with-morph) 11 | - [Bonus: Configuring Nginx and Let's encrypt](#bonus-configuring-nginx-and-lets-encrypt) 12 | 13 | We'll see how one can leverage the Nix ecosystem to easily deploy web applications in a declarative and reproducible way. 14 | We'll demonstrate how to get NixOS running on most servers, even when NixOS is not officially supported by the hosting provider. 15 | The application we are about to deploy is a simple Haskell app which requires access to a PostgreSQL database and also a Redis instance, as an excuse to demonstrate how to orchestrate the different building blocks of a deployment. 16 | Let's begin ! 17 | 18 | ## Getting NixOS on a VPS 19 | 20 | Here, we'll use a simple VPS from Hetzner Cloud but the following should work with most providers. 21 | I won't demonstrate the creation of the VPS as it's provider specific and often just requires a few clicks. 22 | We'll start from a Debian installation which I picked from the available choices offered by Hetzner when creating a new server. 23 | I'm now logged in as root in my new VPS, this will be our "blank state": 24 | 25 | ### The NixOS Lustrate method 26 | ~~~bash 27 | Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent 28 | permitted by applicable law. 29 | Last login: Thu Jun 11 19:27:48 2020 from 37.166.144.196 30 | 31 | root@debian-2gb-nbg1-1:~# uname -a 32 | Linux debian-2gb-nbg1-1 4.19.0-8-amd64 #1 SMP Debian 4.19.98-1+deb10u1 (2020-04-27) x86_64 GNU/Linux 33 | ~~~ 34 | 35 | What we want to achieve now is replacing the current Debian installation with a NixOS one. To do this we'll be using the **_NIXOS_LUSTRATE_** method as explained in the [section of the NixOS manual](https://nixos.org/nixos/manual/#sec-installing-from-other-distro). 36 | First a quick look at the current disk partitionning: 37 | ~~~bash 38 | root@debian-2gb-nbg1-1:~# lsblk 39 | NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT 40 | sda 8:0 0 19.1G 0 disk 41 | ├─sda1 8:1 0 19G 0 part / 42 | ├─sda14 8:14 0 1M 0 part 43 | └─sda15 8:15 0 122M 0 part /boot/efi 44 | sr0 11:0 1 1024M 0 rom 45 | ~~~ 46 | 47 | Let's add a user to run all the installation steps as running it as root triggers some warnings: 48 | ~~~bash 49 | root@debian-2gb-nbg1-1:~# adduser romain 50 | Adding user `romain' ... 51 | Adding new group `romain' (1000) ... 52 | Adding new user `romain' (1000) with group `romain' ... 53 | Creating home directory `/home/romain' ... 54 | Copying files from `/etc/skel' ... 55 | New password: 56 | Retype new password: 57 | passwd: password updated successfully 58 | Changing the user information for romain 59 | Enter the new value, or press ENTER for the default 60 | Full Name []: 61 | Room Number []: 62 | Work Phone []: 63 | Home Phone []: 64 | Other []: 65 | Is the information correct? [Y/n] 66 | ~~~ 67 | We then add our new user to the sudo group to be able to proceed with the next steps: 68 | ~~~bash 69 | root@debian-2gb-nbg1-1:~# usermod -aG sudo romain 70 | ~~~ 71 | We switch to the new user: 72 | ~~~bash 73 | su - romain 74 | ~~~ 75 | Now we can begin the nix installation process as usual: 76 | ~~~bash 77 | curl https://nixos.org/nix/install | sh 78 | . $HOME/.nix-profile/etc/profile.d/nix.sh 79 | ~~~ 80 | Once this is done, we can see that we are currently subscribed to the **nixpkgs-unstable** channel: 81 | ~~~bash 82 | nix-channel --list 83 | nixpkgs https://nixos.org/channels/nixpkgs-unstable 84 | ~~~ 85 | We should probably use a stable channel on a server, so let's replace **nixpkgs-unstable** with the latest nixos channel. That's **nixos-20.03** at the time I write this blog post. 86 | ~~~bash 87 | nix-channel --add https://nixos.org/channels/nixos-20.03 nixpkgs 88 | ~~~ 89 | We then update just to be sure we are using the latest sources: 90 | ~~~bash 91 | nix-channel --update 92 | ~~~ 93 | It's time to install the NixOS installation utilities **nixos-generate-config**, **nixos-install** and **nixos-enter**: 94 | ~~~bash 95 | nix-env -iE "_: with import { configuration = {}; }; with config.system.build; [ nixos-generate-config nixos-install nixos-enter ]" 96 | ~~~ 97 | Once it's done, we have to generate NixOS configuration files. 98 | ~~~bash 99 | romain@debian-2gb-nbg1-1:~$ sudo `which nixos-generate-config` --root / 100 | writing /etc/nixos/hardware-configuration.nix... 101 | writing /etc/nixos/configuration.nix... 102 | ~~~ 103 | You may have encountered the following warnings when invoking the previous command (I have). 104 | ~~~bash 105 | perl: warning: Setting locale failed. 106 | perl: warning: Please check that your locale settings: 107 | LANGUAGE = (unset), 108 | LC_ALL = (unset), 109 | LANG = "en_US.UTF-8" 110 | are supported and installed on your system. 111 | perl: warning: Falling back to the standard locale ("C"). 112 | ~~~ 113 | This doesn't seem to have any noticeable impact down the line so let's proceed. 114 | 115 | Let's now take a look at those two files **nixos-generate-config** created for us. 116 | 117 | **/etc/nixos/configuration.nix** 118 | ~~~nix 119 | # Edit this configuration file to define what should be installed on 120 | # your system. Help is available in the configuration.nix(5) man page 121 | # and in the NixOS manual (accessible by running ‘nixos-help’). 122 | 123 | { config, pkgs, ... }: 124 | 125 | { 126 | imports = 127 | [ # Include the results of the hardware scan. 128 | ./hardware-configuration.nix 129 | ]; 130 | 131 | # Use the GRUB 2 boot loader. 132 | boot.loader.grub.enable = true; 133 | boot.loader.grub.version = 2; 134 | # boot.loader.grub.efiSupport = true; 135 | # boot.loader.grub.efiInstallAsRemovable = true; 136 | # boot.loader.efi.efiSysMountPoint = "/boot/efi"; 137 | # Define on which hard drive you want to install Grub. 138 | # boot.loader.grub.device = "/dev/sda"; # or "nodev" for efi only 139 | 140 | # networking.hostName = "nixos"; # Define your hostname. 141 | # networking.wireless.enable = true; # Enables wireless support via wpa_supplicant. 142 | 143 | # The global useDHCP flag is deprecated, therefore explicitly set to false here. 144 | # Per-interface useDHCP will be mandatory in the future, so this generated config 145 | # replicates the default behaviour. 146 | networking.useDHCP = false; 147 | networking.interfaces.eth0.useDHCP = true; 148 | 149 | # Configure network proxy if necessary 150 | # networking.proxy.default = "http://user:password@proxy:port/"; 151 | # networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain"; 152 | 153 | # Select internationalisation properties. 154 | # i18n.defaultLocale = "en_US.UTF-8"; 155 | # console = { 156 | # font = "Lat2-Terminus16"; 157 | # keyMap = "us"; 158 | # }; 159 | 160 | # Set your time zone. 161 | # time.timeZone = "Europe/Amsterdam"; 162 | 163 | # List packages installed in system profile. To search, run: 164 | # $ nix search wget 165 | # environment.systemPackages = with pkgs; [ 166 | # wget vim 167 | # ]; 168 | 169 | # Some programs need SUID wrappers, can be configured further or are 170 | # started in user sessions. 171 | # programs.mtr.enable = true; 172 | # programs.gnupg.agent = { 173 | # enable = true; 174 | # enableSSHSupport = true; 175 | # pinentryFlavor = "gnome3"; 176 | # }; 177 | 178 | # List services that you want to enable: 179 | 180 | # Enable the OpenSSH daemon. 181 | # services.openssh.enable = true; 182 | 183 | # Open ports in the firewall. 184 | # networking.firewall.allowedTCPPorts = [ ... ]; 185 | # networking.firewall.allowedUDPPorts = [ ... ]; 186 | # Or disable the firewall altogether. 187 | # networking.firewall.enable = false; 188 | 189 | # Enable CUPS to print documents. 190 | # services.printing.enable = true; 191 | 192 | # Enable sound. 193 | # sound.enable = true; 194 | # hardware.pulseaudio.enable = true; 195 | 196 | # Enable the X11 windowing system. 197 | # services.xserver.enable = true; 198 | # services.xserver.layout = "us"; 199 | # services.xserver.xkbOptions = "eurosign:e"; 200 | 201 | # Enable touchpad support. 202 | # services.xserver.libinput.enable = true; 203 | 204 | # Enable the KDE Desktop Environment. 205 | # services.xserver.displayManager.sddm.enable = true; 206 | # services.xserver.desktopManager.plasma5.enable = true; 207 | 208 | # Define a user account. Don't forget to set a password with ‘passwd’. 209 | # users.users.jane = { 210 | # isNormalUser = true; 211 | # extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user. 212 | # }; 213 | 214 | # This value determines the NixOS release from which the default 215 | # settings for stateful data, like file locations and database versions 216 | # on your system were taken. It‘s perfectly fine and recommended to leave 217 | # this value at the release version of the first install of this system. 218 | # Before changing this value read the documentation for this option 219 | # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html). 220 | system.stateVersion = "20.03"; # Did you read the comment? 221 | 222 | } 223 | ~~~ 224 | 225 | This is your **logical** configuration where you can specify what you want to include in your system regardless of the physical machine you build it on. 226 | 227 | **/etc/nixos/hardware-configuration.nix** 228 | ~~~nix 229 | # Do not modify this file! It was generated by ‘nixos-generate-config’ 230 | # and may be overwritten by future invocations. Please make changes 231 | # to /etc/nixos/configuration.nix instead. 232 | { config, lib, pkgs, ... }: 233 | 234 | { 235 | imports = 236 | [ 237 | ]; 238 | 239 | boot.initrd.availableKernelModules = [ "ata_piix" "virtio_pci" "xhci_pci" "sd_mod" "sr_mod" ]; 240 | boot.initrd.kernelModules = [ ]; 241 | boot.kernelModules = [ ]; 242 | boot.extraModulePackages = [ ]; 243 | 244 | fileSystems."/" = 245 | { device = "/dev/disk/by-uuid/c97a4f38-9c51-4846-af75-775ac08e6a76"; 246 | fsType = "ext4"; 247 | }; 248 | 249 | fileSystems."/boot/efi" = 250 | { device = "/dev/disk/by-uuid/3143-1D20"; 251 | fsType = "vfat"; 252 | }; 253 | 254 | swapDevices = [ ]; 255 | 256 | nix.maxJobs = lib.mkDefault 1; 257 | } 258 | 259 | ~~~ 260 | This is your **physical** configuration which NixOS probed from the current machine. 261 | You can see that it set **nix.maxJobs** to default to 1 as my VPS only has one core. 262 | If you want to learn about all the options available to configure your system, you'll want to browse them here [NixOS options](https://nixos.org/nixos/options#). 263 | For example, the docs tell us the following about **nix.maxJobs**: 264 | _This option defines the maximum number of jobs that Nix will try to build in parallel. The default is 1. You should generally set it to the total number of logical cores in your system (e.g., 16 for two CPUs with 4 cores each and hyper-threading)._ 265 | 266 | 267 | Right, we'll trim down our **configuration.nix** by removing comments and adding needed ssh and network configuration. 268 | ~~~nix 269 | { config, pkgs, ... }: 270 | 271 | { 272 | 273 | ... 274 | 275 | networking.usePredictableInterfaceNames = false; 276 | 277 | ... 278 | 279 | services.openssh.enable = true; 280 | 281 | # In case you want to be able to login as root without public key: 282 | # services.openssh.permitRootLogin = "yes"; 283 | # nix-shell -p mkpasswd --run 'mkpasswd -m sha-512 nixosdemo' 284 | # users.users.root.initialHashedPassword = "$6$ElZ9YNBW/hBJYjo$7TSbGs.H0abwIHUJe3zPQ8NScs6AKOKB7Br6TBEZk.vgZ7J9neAVL8CbTIGOPfW8oirGP1b6kRErQvo/r9jmX1"; 285 | 286 | users.users.root.openssh.authorizedKeys.keys = [ 287 | "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICRH41b8Qn+80OJuZssDDfqcSkH3MkVyqoA4I8V2FkW7 romain" 288 | ]; 289 | 290 | ... 291 | } 292 | ~~~ 293 | First, we **disable the network predictable interface names** since we don't know yet how they will be named after NixOS takes over Debian. 294 | We want our ethernet access to be named eth0 to be able to specify the useDHCP option for it for now. 295 | That's important if you don't want your machine to lose access to internet after the reboot we'll do. 296 | Second, we enable openssh daemon to be able to login to our server after NixOS takeover, the current root password won't be kept so we specify our public key for ssh. 297 | If you want to login as root using a password, you can uncomment the lines, I've given the needed command to setup "nixosdemo" as initial root password. 298 | You'll also need to change the **permitRootLogin** option to **"yes"** as, by default, you won't be able to login as root via ssh since the default option is **"prohibit-password"**. See [here](https://nixos.org/nixos/options#permitrootlogin). 299 | 300 | Next, we'll add a small tweak to our **hardware-configuration.nix** by specifying the device GRUB will use: 301 | 302 | ~~~nix 303 | # Do not modify this file! It was generated by ‘nixos-generate-config’ 304 | # and may be overwritten by future invocations. Please make changes 305 | # to /etc/nixos/configuration.nix instead. 306 | { config, lib, pkgs, ... }: 307 | 308 | { 309 | ... 310 | 311 | boot.loader.grub.device = "/dev/sda"; 312 | 313 | ... 314 | } 315 | ~~~ 316 | We can now proceed with the remaining steps documented in the manual. 317 | First, we build the system environment 318 | ~~~bash 319 | nix-env -p /nix/var/nix/profiles/system -f '' -I nixos-config=/etc/nixos/configuration.nix -iA system 320 | ~~~ 321 | We change ownership of the /nix tree to root 322 | ~~~bash 323 | sudo chown -R 0.0 /nix 324 | ~~~ 325 | We create the /etc/NIXOS file which indicates that we are running a NixOS installation. 326 | ~~~bash 327 | sudo touch /etc/NIXOS 328 | ~~~ 329 | We create a NIXOS\_LUSTRATE file which is how we tell NixOS to clean the root partition at boot. 330 | The content of this file indicates which locations the cleaning process must preserve, so we add our **/etc/nixos** directory to it since it contains our **configuration.nix** and **hardware-configuration.nix** files. 331 | ~~~bash 332 | sudo touch /etc/NIXOS_LUSTRATE 333 | echo etc/nixos | sudo tee -a /etc/NIXOS_LUSTRATE 334 | ~~~ 335 | **If your hosting provider has a snapshot option, now would be a good time to backup the state of your machine in case anything goes wrong so that you won't have to get though the whole process again.** 336 | We now move the Debian boot files and use NixOS **switch-to-configuration** to activate the new system at next boot. 337 | ~~~bash 338 | sudo mv -v /boot /boot.bak 339 | sudo /nix/var/nix/profiles/system/bin/switch-to-configuration boot 340 | ~~~ 341 | Switch back to root and reboot 342 | ~~~bash 343 | exit 344 | reboot 345 | ~~~ 346 | 347 | Now, if everything goes well, you should be able to ssh as root to your new NixOS machine 348 | ~~~bash 349 | [romain@clevo-N141ZU:~]$ ssh root@159.69.155.132 350 | [root@nixos:~]# uname -a 351 | Linux nixos 5.4.45 #1-NixOS SMP Sun Jun 7 11:18:52 UTC 2020 x86_64 GNU/Linux 352 | ~~~ 353 | 354 | You can now remove the old Debian files placed in **/old-root** 355 | ~~~bash 356 | rm -rf /old-root 357 | ~~~ 358 | 359 | Update your machine using NixOS machine 360 | ~~~bash 361 | nixos-rebuild switch --upgrade 362 | ~~~ 363 | 364 | Clean the store 365 | ~~~bash 366 | nix-collect-garbage -d 367 | ~~~ 368 | 369 | Congratulations ! You now have a clean **NixOS 20.03** installation. 370 | Again, if your hosting provider has a snapshot options, **now would be a good time to make a snapshot so that you could reuse that setup on multiple servers**. 371 | 372 | ### Alternative ways to get NixOS on your server 373 | 374 | The above method has served us well since we were fine with keeping the existing partitionning scheme. 375 | In case you have more specific needs, you may have to resort to other methods, I'll list some here to get you started: 376 | * [NixOS-infect](https://github.com/elitak/nixos-infect) 377 | * [kexec method](https://nixos.wiki/wiki/Install_NixOS_on_Scaleway_X86_Virtual_Cloud_Server) geared toward Scaleway servers but should be easily tweaked to fit your provider. 378 | * Some providers provide out of the box NixOS installations, that's the case with Hetzner but I wanted to demonstrate a more involved process 379 | 380 | ## A simple Haskell web service 381 | 382 | I've put up a simple haskell web app which you can see on its [github repository](https://github.com/aveltras/deploy-app-with-nixos). 383 | It uses [hedis](https://hackage.haskell.org/package/hedis) to access a Redis server and [hasql](https://hackage.haskell.org/package/hasql) to provide PostgreSQL access. 384 | The database schema is handled by an external migration tool written in Go : [dbmate](https://github.com/amacneil/dbmate). 385 | The app is simply incrementing two values each time you visit the root path. It's not what you'd call pretty but this isn't the purpose of this post. 386 | Here's the code from **Main.hs**: 387 | 388 | ~~~haskell 389 | {-# LANGUAGE LambdaCase #-} 390 | {-# LANGUAGE OverloadedStrings #-} 391 | {-# LANGUAGE QuasiQuotes #-} 392 | {-# LANGUAGE ScopedTypeVariables #-} 393 | {-# LANGUAGE TemplateHaskell #-} 394 | 395 | module Main where 396 | 397 | import qualified Data.ByteString as B 398 | import qualified Data.ByteString.Char8 as C8 399 | import qualified Data.ByteString.Lazy as BL 400 | import Data.Either (fromRight) 401 | import Data.Maybe (fromMaybe) 402 | import qualified Database.Redis as Redis 403 | import GHC.Int 404 | import qualified Hasql.Connection as Hasql 405 | import qualified Hasql.Session as Hasql 406 | import qualified Hasql.Statement as Hasql 407 | import Hasql.TH 408 | import Network.HTTP.Types 409 | import Network.Wai 410 | import Network.Wai.Handler.Warp 411 | import System.Environment (getEnv) 412 | 413 | main :: IO () 414 | main = do 415 | 416 | port :: Int <- read <$> getEnv "APP_PORT" 417 | dbConnStr <- getEnv "DATABASE_URL" 418 | 419 | redisConn <- Redis.checkedConnect Redis.defaultConnectInfo 420 | sqlConn <- either (error . C8.unpack . fromMaybe "db connection error") id <$> Hasql.acquire (C8.pack dbConnStr) 421 | 422 | run port $ app $ server sqlConn redisConn 423 | 424 | app :: (B.ByteString -> IO (Status, BL.ByteString)) -> Application 425 | app handler request sendResponse = do 426 | (status, bs) <- handler $ rawPathInfo request 427 | sendResponse $ responseLBS status [(hContentType, "text/plain")] bs 428 | 429 | server :: Hasql.Connection -> Redis.Connection -> B.ByteString -> IO (Status, BL.ByteString) 430 | server sqlConn redisConn = \case 431 | "/" -> do 432 | sqlVal <- getSqlValue sqlConn 433 | redisVal <- getRedisValue redisConn 434 | pure (ok200, "Current redis value: " <> BL.fromStrict redisVal <> "\nCurrent sql value: " <> (BL.fromStrict . C8.pack . show) sqlVal) 435 | _ -> pure (notFound404, "not found") 436 | 437 | getSqlValue :: Hasql.Connection -> IO Int32 438 | getSqlValue sqlConn = do 439 | 440 | maybeInt <- fmap (either (error . show) id) $ flip Hasql.run sqlConn $ 441 | Hasql.statement () [maybeStatement| 442 | SELECT intval :: int4 443 | FROM value 444 | |] 445 | 446 | case maybeInt of 447 | 448 | Nothing -> do 449 | res <- flip Hasql.run sqlConn $ Hasql.statement 0 450 | [singletonStatement| 451 | INSERT INTO value (intval) 452 | VALUES ($1 :: int4) 453 | RETURNING intval :: int4 454 | |] 455 | 456 | pure $ unwrap res 457 | 458 | Just int -> do 459 | res <- flip Hasql.run sqlConn $ Hasql.statement () 460 | [singletonStatement| 461 | UPDATE value SET 462 | intval = intval + 1 :: int4 463 | RETURNING intval :: int4 464 | |] 465 | 466 | pure $ unwrap res 467 | 468 | 469 | where 470 | unwrap :: Either Hasql.QueryError a -> a 471 | unwrap = \case 472 | Left err -> error $ show err 473 | Right a -> a 474 | 475 | getRedisValue :: Redis.Connection -> IO B.ByteString 476 | getRedisValue redisConn = 477 | Redis.runRedis redisConn $ do 478 | bsVal <- unwrap <$> Redis.get "app:strval" 479 | case bsVal of 480 | Nothing -> do 481 | Redis.set "app:strval" "" 482 | pure "" 483 | Just val -> do 484 | let newVal = val <> "x" 485 | _ <- unwrap <$> Redis.set "app:strval" newVal 486 | pure newVal 487 | 488 | where 489 | unwrap :: Either Redis.Reply a -> a 490 | unwrap = \case 491 | Left err -> error $ show err 492 | Right val -> val 493 | 494 | ~~~ 495 | 496 | As you can see, we'll need configuration values provided as environment variables here, for the port on which to serve our app and for the PostgreSQL connection string. 497 | 498 | Let's run our app locally to get a quick sense of what's provided: 499 | 500 | ~~~bash 501 | cabal run server 502 | ~~~ 503 | 504 | Now in another shell 505 | 506 | ~~~bash 507 | [romain@clevo-N141ZU:~]$ curl http://localhost:3003 508 | Current redis value: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 509 | Current sql value: 67 510 | [romain@clevo-N141ZU:~]$ curl http://localhost:3003 511 | Current redis value: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 512 | Current sql value: 68 513 | ~~~ 514 | 515 | Later in this blog post, we'll demonstrate how to package our app to run it on NixOS with its required dependencies. 516 | 517 | ## Introducing NixOS modules 518 | 519 | Now that you have a NixOS installation running, it's time to explain how it works. 520 | We'll build on the [previous blog post](https://github.com/aveltras/setting-up-a-haskell-development-environment-with-nix) so I won't rehash the basics of Nix here, feel free to take a break and read it first if you haven't done so already and are unfamiliar with Nix. 521 | **NixOS modules** are the building blocks from which you assemble your desired system configuration. Everything in NixOS is a module, from your OS configuration files we encountered in the previous section to the available options at [NixOS options](https://nixos.org/nixos/options.html). 522 | 523 | The basic structure of a module is the following: 524 | 525 | ~~~nix 526 | { config, pkgs, ... }: 527 | { 528 | imports = [ 529 | # paths to other modules 530 | ]; 531 | 532 | options = { 533 | # option declarations 534 | }; 535 | 536 | config = { 537 | # option definitions 538 | }; 539 | } 540 | ~~~ 541 | 542 | The first line is optional but it turns out you'll find it in most non trivial NixOS modules. 543 | This is the way you can access your global machine configuration and other things such as the pkgs of your subscribed channel. 544 | Those are automatically provided by the NixOS configuration building script, you don't have to pass them around yourself. 545 | For example, the **config** parameter here is a reference to **the whole configuration as it's being built**, this enables you to configure your module differently based on the configuration of other modules. 546 | **Let's now explain what each of the 3 sections is used for:** 547 | * **imports** is a way to include other modules in your configuration. We've already met this with our **configuration.nix** above which is the entry point of our system configuration. There, we imported our **hardware-configuration.nix** which itself imports another NixOS module 548 | ~~~nix 549 | # /etc/nixos/configuration.nix 550 | imports = 551 | [ # Include the results of the hardware scan. 552 | ./hardware-configuration.nix 553 | ]; 554 | 555 | # /etc/nixos/hardware-configuration.nix 556 | imports = 557 | [ 558 | ]; 559 | ~~~ 560 | NixOS is responsible for merging all those configurations and providing them the things they need like **config**, **pkgs**.. 561 | * **options** is the way we can provide configuration options for our modules. The first of them often being a boolean **enable** option which will determine in the next section if the module is to be activated or not. 562 | NixOS provides a basic type system we can leverage to ensure the values provided as configuration to our module fullfill some requirements. We'll explain it further with a simple module example below but if you want to learn more, you can refer to the [section about option types](https://nixos.org/nixos/manual/index.html#sec-option-types) in the manual. 563 | * **config** is the way our module provide its functionality to the whole system. It is usually guarded by a boolean test on the **enable** option as we'll see in the example below. 564 | 565 | ### A quick example : MailHog mail catching facility 566 | 567 | Let's deepen our understanding with a small example taken from the available NixOS services. 568 | I've selected the [MailHog](https://github.com/mailhog/MailHog) preconfigured module. 569 | This is a tool you'd use when NixOS is your development machine since **NixOS is not only geared toward servers, you can use it as your desktop daily driver too**, that's what I personally use. 570 | Alright, let's search for mailhog in the available options, you'll find [two results](https://nixos.org/nixos/options.html#mailhog). If you click on one of them, you'll see that the docs link to the module in the github repo: [https://github.com/NixOS/nixpkgs/blob/release-20.03/nixos/modules/services/mail/mailhog.nix](https://github.com/NixOS/nixpkgs/blob/release-20.03/nixos/modules/services/mail/mailhog.nix). 571 | I'll reproduce the module content here in case it's modified after this article is written. 572 | ~~~nix 573 | # https://raw.githubusercontent.com/NixOS/nixpkgs/release-20.03/nixos/modules/services/mail/mailhog.nix 574 | { config, lib, pkgs, ... }: 575 | 576 | with lib; 577 | 578 | let 579 | cfg = config.services.mailhog; 580 | in { 581 | ###### interface 582 | 583 | options = { 584 | 585 | services.mailhog = { 586 | enable = mkEnableOption "MailHog"; 587 | user = mkOption { 588 | type = types.str; 589 | default = "mailhog"; 590 | description = "User account under which mailhog runs."; 591 | }; 592 | }; 593 | }; 594 | 595 | 596 | ###### implementation 597 | 598 | config = mkIf cfg.enable { 599 | 600 | users.users.mailhog = { 601 | name = cfg.user; 602 | description = "MailHog service user"; 603 | isSystemUser = true; 604 | }; 605 | 606 | systemd.services.mailhog = { 607 | description = "MailHog service"; 608 | after = [ "network.target" ]; 609 | wantedBy = [ "multi-user.target" ]; 610 | serviceConfig = { 611 | Type = "simple"; 612 | ExecStart = "${pkgs.mailhog}/bin/MailHog"; 613 | User = cfg.user; 614 | }; 615 | }; 616 | }; 617 | } 618 | ~~~ 619 | There we can see that the two options found in the browsable catalog correspond to the two entries in the **options** section of the module. 620 | We have the classic **services.mailhog.enable** option which dictates if the module is in use and we have a **services.mailhog.user** option which we can leverage to specify which NixOS user we want the service to be ran under. 621 | The **user** option is of type **types.str** which means it should be a string and it provides a default value of **mailhog** in case we don't specify one when enabling the module in our global configuration. 622 | We then have the **config section** guarded by a **mkIf cfg.enable** clause which means the module won't have any effect if it's included but not enabled explicitly in out configuration (or in another module enabled in our config which requires it). 623 | Activating the **MailHog module** will have two effects: 624 | * **First**, it will create a new user on the system 625 | * **Second**, it will create a systemd services to run the MailHog app automatically. It leverages the mailhog package present in Nixpkgs which you can find [here](https://github.com/NixOS/nixpkgs-channels/blob/nixos-20.03/pkgs/servers/mail/mailhog/default.nix). 626 | Right, this will conclude our introduction to NixOS modules, we'll take some inspiration from the MailHog module to create the module for our app. 627 | 628 | You can find more information about NixOS modules here: 629 | * [NixOS wiki module entry](https://nixos.wiki/wiki/Module) 630 | * [NixOS manual section about writing modules](https://nixos.org/nixos/manual/index.html#sec-writing-modules) 631 | 632 | ## Packaging your app as a NixOS module 633 | Right, equipped with your fresh knowledge, now's the time to package our app as a NixOS module. 634 | I won't paste all the existing code here so feel free to browse the sources directly on Github [deploy-app-with-nixos](https://github.com/aveltras/deploy-app-with-nixos) 635 | Our **default.nix** exposes 2 attributes of interest for our current endeavour: 636 | * **server** which is our haskell package 637 | * **migrations** which is a simple package containing our sql migration scripts 638 | Let's build those locally so that you get a good intuition of what that means. 639 | 640 | **First**, the server package.. 641 | 642 | ~~~bash 643 | [romain@clevo-N141ZU:~/Code/deploy-app-with-nixos]$ nix-build -A server 644 | *** Elided build ouput for brievity *** 645 | /nix/store/mk8ismjdd7wd254as18za982wjb5l26c-server-0.1.0 646 | ~~~ 647 | 648 | As usual, the nix-build in its default invocation creates a **result** symlink in the current directory we can inspect. 649 | 650 | ~~~bash 651 | [romain@clevo-N141ZU:~/Code/deploy-app-with-nixos]$ tree result 652 | result 653 | └── bin 654 | └── server 655 | 656 | 1 directory, 1 file 657 | ~~~ 658 | 659 | **Second**, the migrations package.. 660 | 661 | ~~~bash 662 | [romain@clevo-N141ZU:~/Code/deploy-app-with-nixos]$ nix-build -A migrations 663 | unpacking 'https://github.com/NixOS/nixpkgs-channels/archive/nixos-20.03.tar.gz'... 664 | /nix/store/isfy6krvc606p48alcxw6dkk7jx2b7fs-mkMigrations 665 | ~~~ 666 | ~~~bash 667 | [romain@clevo-N141ZU:~/Code/deploy-app-with-nixos]$ tree result 668 | result 669 | └── 20200611140605_setup.sql 670 | 671 | 0 directories, 1 file 672 | ~~~ 673 | The resulting package contains only our migrations files. 674 | We have all we need to start building our module. 675 | 676 | Let's create a basic **module.nix** in the source directory of our haskell package. 677 | ~~~nix 678 | { config, lib, pkgs, ... }: 679 | 680 | with lib; 681 | 682 | let 683 | 684 | cfg = config.services.myapp; 685 | 686 | in { 687 | 688 | options = { 689 | services.myapp.enable = mkEnableOption "My App"; 690 | }; 691 | 692 | config = mkIf cfg.enable { 693 | 694 | services = { 695 | postgresql = { 696 | enable = true; 697 | enableTCPIP = true; 698 | }; 699 | 700 | redis.enable = true; 701 | }; 702 | 703 | }; 704 | } 705 | ~~~ 706 | In its current form, our module only defines a **services.myapp.enable** option which, when given **true** in our machine configuration will enable the **redis** and **postgresql** modules provided by NixOS. 707 | As usual, you can browse the options to learn more about the available options for those two modules: 708 | * [Redis](https://nixos.org/nixos/options.html#services.redis.) 709 | * [PostgreSQL](https://nixos.org/nixos/options.html#services.postgresql) 710 | 711 | An important thing to notice is the **cfg = config.services.myapp;** binding, that's how the module will reference its own configuration throughout the file. You'll see what I mean in a moment. 712 | 713 | If you were to use this module in its current state, enabling it would simply enable a PostgreSQL and a Redis service on your machine. 714 | 715 | Let's add our server package to the mix with a first round of modifications. 716 | 717 | ~~~nix 718 | { config, lib, pkgs, ... }: 719 | 720 | with lib; 721 | 722 | let 723 | 724 | pkg = import ./.; 725 | 726 | server = pkg.server; 727 | 728 | ... 729 | 730 | in { 731 | 732 | ... 733 | 734 | config = mkIf cfg.enable { 735 | 736 | ... 737 | 738 | services = { 739 | postgresql = { 740 | enable = true; 741 | enableTCPIP = true; 742 | }; 743 | 744 | redis.enable = true; 745 | }; 746 | 747 | systemd.services = { 748 | 749 | myapp = { 750 | wantedBy = [ "multi-user.target" ]; 751 | description = "Start my app server."; 752 | after = [ "network.target" ]; 753 | 754 | serviceConfig = { 755 | Type = "simple"; 756 | User = "myapp"; 757 | ExecStart = ''${server}/bin/server''; 758 | Restart = "always"; 759 | KillMode = "process"; 760 | }; 761 | }; 762 | }; 763 | 764 | }; 765 | } 766 | ~~~ 767 | 768 | We now have imported our **default.nix** in the module and bound the server variable to our haskell server package. 769 | We have also added a systemd service for your web server. 770 | You can find information on defining systemd services by looking at the options [here](https://nixos.org/nixos/options.html#systemd.services.%3Cname%3E). 771 | 772 | If you were to use this module right now, the app would crash since it isn't given its needed environment variables and there is currently no database created in PostgreSQL. 773 | 774 | Let's make a new round of modifications. 775 | 776 | ~~~nix 777 | { config, lib, pkgs, ... }: 778 | 779 | with lib; 780 | 781 | let 782 | 783 | ... 784 | 785 | migrations = pkg.migrations; 786 | 787 | ... 788 | 789 | in { 790 | 791 | options = { 792 | services.myapp = { 793 | 794 | ... 795 | 796 | user = mkOption { 797 | type = types.str; 798 | default = "myapp"; 799 | description = "User account under which my app runs."; 800 | }; 801 | 802 | port = mkOption { 803 | type = types.nullOr types.int; 804 | default = 3003; 805 | example = 8080; 806 | description = '' 807 | The port to bind my app server to. 808 | ''; 809 | }; 810 | }; 811 | }; 812 | 813 | config = mkIf cfg.enable { 814 | 815 | users.users.${cfg.user} = { 816 | name = cfg.user; 817 | description = "My app service user"; 818 | isSystemUser = true; 819 | }; 820 | 821 | networking.firewall.allowedTCPPorts = [ cfg.port ]; 822 | 823 | services = { 824 | postgresql = { 825 | 826 | enable = true; 827 | 828 | enableTCPIP = true; 829 | ensureDatabases = [ cfg.user ]; 830 | 831 | ensureUsers = [ 832 | { 833 | name = cfg.user; 834 | ensurePermissions = { 835 | "DATABASE ${cfg.user}" = "ALL PRIVILEGES"; 836 | }; 837 | } 838 | ]; 839 | 840 | authentication = pkgs.lib.mkOverride 10 '' 841 | local sameuser all peer 842 | host sameuser all ::1/32 trust 843 | ''; 844 | }; 845 | 846 | redis.enable = true; 847 | }; 848 | 849 | systemd.services = { 850 | 851 | db-migration = { 852 | description = "DB migrations script"; 853 | wantedBy = [ "multi-user.target" ]; 854 | after = [ "postgresql.service" ]; 855 | requires = [ "postgresql.service" ]; 856 | 857 | serviceConfig = { 858 | User = cfg.user; 859 | Type = "oneshot"; 860 | ExecStart = "${pkgs.dbmate}/bin/dbmate -d ${migrations} --no-dump-schema up"; 861 | }; 862 | }; 863 | 864 | myapp = { 865 | wantedBy = [ "multi-user.target" ]; 866 | description = "Start my app server."; 867 | after = [ "network.target" ]; 868 | requires = [ "db-migration.service" ]; 869 | 870 | serviceConfig = { 871 | Type = "simple"; 872 | User = cfg.user; 873 | ExecStart = ''${server}/bin/server''; 874 | Restart = "always"; 875 | KillMode = "process"; 876 | }; 877 | }; 878 | }; 879 | }; 880 | } 881 | 882 | ~~~ 883 | There's a lot of changes here. We will now be using our **migrations** packages. 884 | We have also defined configuration options for our module, **we can now specify the system user and the port to use**. 885 | Those two are referenced in the rest of the file thanks to the **cfg** mentionned earlier. 886 | We have opened the firewall port in use by our app for it to be accessible from the outer world. 887 | Our PostgreSQL configuration now **ensures that a user and a database with the given username will exist and also configure permissions accordingly**. 888 | It also showcases how to specify a **specific authentication scheme**, here, PostgreSQL will enable both local access (using unix sockets) and access through the local network to the database named like the user trying to access it. 889 | Our previous server systemd service now requires another service before running, the **db-migration service**, which we've just configured. This will ensure that we access an up to date database schema with our app. 890 | The migration service itself requires the PostgreSQL service. 891 | All this is not functional yet since we still need to provide their configuration values to our services, let's do this with a last round of modifications. 892 | 893 | ~~~nix 894 | { config, lib, pkgs, ... }: 895 | 896 | with lib; 897 | 898 | let 899 | ... 900 | in { 901 | 902 | ... 903 | 904 | config = mkIf cfg.enable { 905 | 906 | ... 907 | 908 | systemd.services = { 909 | 910 | db-migration = { 911 | 912 | ... 913 | 914 | environment = { 915 | DATABASE_URL = "postgres://${cfg.user}@localhost:5432/myapp?sslmode=disable"; 916 | }; 917 | 918 | ... 919 | 920 | }; 921 | 922 | myapp = { 923 | 924 | ... 925 | 926 | environment = { 927 | APP_PORT = toString cfg.port; 928 | DATABASE_URL = "postgres:///${cfg.user}"; 929 | }; 930 | 931 | ... 932 | 933 | }; 934 | }; 935 | }; 936 | } 937 | ~~~ 938 | 939 | We are now providing our two services with their configuration using the environment attribute. 940 | The migration service will connect through the local network as **dbmate** doesn't yet support connecting through a unix socket. Our app will connect through a socket. 941 | 942 | Right, this conclude our module definition. 943 | **A thing to note here is that we didn't have to handle providing configuration files containing secrets.** You can include them directly in your configuration but know that those files will be put in the nix store somewhere and the store is readable by other users on the machine, something to keep in mind if you're using a shared environment. 944 | **Solutions exist for this problem though**, the tool we'll use in the next section provide some functionality to address this problem. 945 | 946 | ## Deploying with Morph 947 | 948 | In order to deploy our configuration, we'll leverage a tool called Morph, you can find more information on its [github repository](https://github.com/DBCDK/morph). 949 | Other solutions exist such as [Nixops](https://github.com/NixOS/nixops) or you could even use hand rolled scripts as described in [Industrial-strength Deployments in Three Commands](https://vaibhavsagar.com/blog/2019/08/22/industrial-strength-deployments/). 950 | I've found Morph simpler to use than Nixops and, more importantly, it doesn't require you to keep deployment state files around. 951 | 952 | Morph is available in the nixpkgs channel so let's install it 953 | ~~~bash 954 | nix-env -i morph 955 | ~~~ 956 | At this point, you should copy your server's **/etc/nixos/configuration.nix** and **/etc/nixos/hardware-configuration.nix** locally as deploying will replace them. 957 | For this example, I've created a **nixos** directory in the project but you may want to store the configurations for your remote servers in their own repositorties, especially if you have multiple app running on a single server. We'll keep things simple here, so let's copy the two configuration files in the **nixos** directory. 958 | 959 | We'll create a **deploy.nix** in the project directory with the following content: 960 | 961 | ~~~nix 962 | let 963 | 964 | pkgs = import (builtins.fetchTarball { 965 | url = "https://github.com/NixOS/nixpkgs-channels/archive/nixos-20.03.tar.gz"; 966 | }) {}; 967 | 968 | in { 969 | 970 | network = { 971 | inherit pkgs; 972 | description = "My NixOS server"; 973 | }; 974 | 975 | "159.69.155.132" = { config, pkgs, ... }: { 976 | 977 | deployment = { 978 | targetUser = "root"; 979 | }; 980 | 981 | imports = [ 982 | ./nixos/configuration.nix 983 | ./module.nix 984 | ]; 985 | 986 | services.myapp = { 987 | enable = true; 988 | port = 8080; 989 | }; 990 | 991 | }; 992 | } 993 | ~~~ 994 | 995 | It's a simple configuration as required by Morph. 996 | We can deploy to multiple machines but here we'll only need one which we'll address by its IP address. 997 | We specify that deployments should be done with the root user and we simply import our existing **configuration.nix** (which will itself import **hardware-configuration.nix**) and our module. 998 | We enable our service and specify a different port than the default one. 999 | This could have been done in the **configuration.nix** but that's not of great importance here, you can organize your configurations how you want. 1000 | We also pin the **nixpkgs version** to the 20.03 archive since that's what is already on our server (subscribed to the 20.03 branch), this will prevent you to upgrade your whole server to a newer NixOS involuntarily (if for example, you're running nixos-unstable on your **desktop machine**). 1001 | 1002 | Let's build our server configuration locally first: 1003 | ~~~bash 1004 | [romain@clevo-N141ZU:~/Code/deploy-app-with-nixos]$ morph build deploy.nix 1005 | Selected 1/1 hosts (name filter:-0, limits:-0): 1006 | 0: 159.69.155.132 (secrets: 0, health checks: 0) 1007 | 1008 | building '/nix/store/kb222zkxdfj9bklayr5q8ma141jjwznx-cabal2nix-server.drv'... 1009 | *** Elided output for brievity *** 1010 | nix result path: 1011 | /nix/store/vxg1plm65wzvxk6vz4k1n1ks9gy5jgpq-morph 1012 | ~~~ 1013 | We can see what the system build looks like by browsing the store entry the build returned. 1014 | 1015 | ~~~bash 1016 | [romain@clevo-N141ZU:~/Code/deploy-app-with-nixos]$ tree /nix/store/vxg1plm65wzvxk6vz4k1n1ks9gy5jgpq-morph/159.69.155.132 1017 | /nix/store/vxg1plm65wzvxk6vz4k1n1ks9gy5jgpq-morph/159.69.155.132 1018 | ├── activate 1019 | ├── append-initrd-secrets -> /nix/store/z6kk89lds89jawpv0zy471gfq56hf43j-append-initrd-secrets/bin/append-initrd-secrets 1020 | ├── bin 1021 | │ └── switch-to-configuration 1022 | ├── configuration-name 1023 | ├── etc -> /nix/store/ls4qbbl0h6qkd6bn71n798x2i8sys71f-etc/etc 1024 | ├── extra-dependencies 1025 | ├── fine-tune 1026 | ├── firmware -> /nix/store/933miznriml41c98rrid6zxpyqqjgqx2-firmware/lib/firmware 1027 | ├── init 1028 | ├── init-interface-version 1029 | ├── initrd -> /nix/store/46x17f67k9rmhqpmrysv08bpg1ndp5gg-initrd-linux-5.4.45/initrd 1030 | ├── kernel -> /nix/store/2rwn0zkprhcjr9psgrs79ci2jgs6i2f6-linux-5.4.45/bzImage 1031 | ├── kernel-modules -> /nix/store/m6v1a0bk5x70a63nnqbfiqqs7p7mx9ys-kernel-modules 1032 | ├── kernel-params 1033 | ├── nixos-version 1034 | ├── sw -> /nix/store/h2h3jncpysal5q05krnf7pk6fbp0dnvv-system-path 1035 | ├── system 1036 | └── systemd -> /nix/store/vfzp1mavwiz5w3v10hs69962k0gwl26c-systemd-243.7 1037 | 1038 | 7 directories, 12 files 1039 | ~~~ 1040 | 1041 | It's time to deploy our configuration. For this we'll run the following: 1042 | ~~~bash 1043 | [romain@clevo-N141ZU:~/Code/deploy-app-with-nixos]$ morph deploy deploy.nix test 1044 | Selected 1/1 hosts (name filter:-0, limits:-0): 1045 | 0: 159.69.155.132 (secrets: 0, health checks: 0) 1046 | 1047 | /nix/store/vxg1plm65wzvxk6vz4k1n1ks9gy5jgpq-morph 1048 | nix result path: 1049 | /nix/store/vxg1plm65wzvxk6vz4k1n1ks9gy5jgpq-morph 1050 | 1051 | Pushing paths to 159.69.155.132 (root@159.69.155.132): 1052 | * /nix/store/a8q5qb4pqfnbdxbp0hy4wws82gfd02v6-nixos-system-159.69.155.132-20.03post-git 1053 | Enter passphrase for key '/home/romain/.ssh/id_ed25519': 1054 | [52 copied (69.7 MiB)] 1055 | 1056 | Executing 'test' on matched hosts: 1057 | 1058 | ** 159.69.155.132 1059 | Enter passphrase for key '/home/romain/.ssh/id_ed25519': 1060 | stopping the following units: nscd.service, systemd-sysctl.service 1061 | NOT restarting the following changed units: systemd-fsck@dev-disk-by\x2duuid-3143\x2d1D20.service 1062 | activating the configuration... 1063 | setting up /etc... 1064 | reloading user units for root... 1065 | setting up tmpfiles 1066 | reloading the following units: dbus.service, firewall.service 1067 | starting the following units: nscd.service, systemd-sysctl.service 1068 | the following new units were started: myapp.service, postgresql.service, redis.service 1069 | 1070 | Running healthchecks on 159.69.155.132 (159.69.155.132): 1071 | Health checks OK 1072 | Done: 159.69.155.132 1073 | ~~~ 1074 | 1075 | Here I've deployed using **test**, you have several options for the deploy command as stated on Morph github readme. 1076 | _Notably, morph deploy requires a . The switch-action must be one of dry-activate, test, switch or boot corresponding to nixos-rebuild arguments of the same name. Refer to the [NixOS manual](https://nixos.org/nixos/manual/index.html#sec-changing-config) for a detailed description of switch-actions._ 1077 | 1078 | We can now test it from out desktop machine: 1079 | ~~~bash 1080 | [romain@clevo-N141ZU:~/Code/deploy-app-with-nixos]$ curl http://159.69.155.132:8080/ 1081 | Current redis value: x 1082 | Current sql value: 1 1083 | [romain@clevo-N141ZU:~/Code/deploy-app-with-nixos]$ curl http://159.69.155.132:8080/ 1084 | Current redis value: xx 1085 | Current sql value: 2 1086 | ~~~ 1087 | 1088 | **I should probably state here that it didn't work right directly. For some reason, the app server could not connect to the PostgreSQL socket.** 1089 | That was somehow easy to debug though, I've just ssh'ed on the server and ran : 1090 | ~~~bash 1091 | [root@159:~]# systemctl status myapp.service 1092 | ● myapp.service - Start my app server. 1093 | Loaded: loaded (/nix/store/74pg5qwlikn130b1fgijkwp9bqvccdn1-unit-myapp.service/myapp.service; enabled; vendor preset: enabled) 1094 | Active: active (running) since Fri 2020-06-12 17:21:55 CEST; 3min 30s ago 1095 | Main PID: 2061 (server) 1096 | IP: 1.1K in, 1.3K out 1097 | Tasks: 1 (limit: 2316) 1098 | Memory: 1.5M 1099 | CPU: 528ms 1100 | CGroup: /system.slice/myapp.service 1101 | └─2061 /nix/store/b6va2qkkf8pdqc59ndhvxcrmdc8rbm58-server-0.1.0/bin/server 1102 | 1103 | juin 12 17:24:29 159.69.155.132 server[2061]: could not connect to server: No such file or directory 1104 | juin 12 17:24:29 159.69.155.132 server[2061]: Is the server running locally and accepting 1105 | juin 12 17:24:29 159.69.155.132 server[2061]: connections on Unix domain socket "/run/postgresql/.s.PGSQL.5432"? 1106 | juin 12 17:24:29 159.69.155.132 server[2061]: CallStack (from HasCallStack): 1107 | juin 12 17:24:29 159.69.155.132 server[2061]: error, called at src/Main.hs:32:22 in main:Main 1108 | juin 12 17:25:14 159.69.155.132 server[2061]: could not connect to server: No such file or directory 1109 | juin 12 17:25:14 159.69.155.132 server[2061]: Is the server running locally and accepting 1110 | juin 12 17:25:14 159.69.155.132 server[2061]: connections on Unix domain socket "/run/postgresql/.s.PGSQL.5432"? 1111 | juin 12 17:25:14 159.69.155.132 server[2061]: CallStack (from HasCallStack): 1112 | juin 12 17:25:14 159.69.155.132 server[2061]: error, called at src/Main.hs:32:22 in main:Main 1113 | ~~~ 1114 | 1115 | Restarting PostgreSQL service and then the myapp.service fixed it. 1116 | 1117 | **To finalize your configuration, don't forget to morph deploy with the switch option instead of test.** 1118 | 1119 | As mentioned previously, Morph provides some secrets handling functionality which you can learn about on their [github readme](https://github.com/DBCDK/morph#secrets). 1120 | That's good to know if you need it, which is bound to happen if you use NixOS for any non trivial application. 1121 | 1122 | **Alright that's the bulk of what you need to know to be able to deploy your applications to a NixOS server. 1123 | We'll explore in the last section how you can put Nginx in front of it and how to setup ssl with Let's Encrypt.** 1124 | 1125 | ## Bonus: Configuring Nginx and Let's encrypt 1126 | 1127 | NixOS provides out of the box support for generating Let's Encrypt certificates so we'll leverage this. We just need to tweak our configuration a bit. 1128 | 1129 | ~~~nix 1130 | let 1131 | 1132 | pkgs = import (builtins.fetchTarball { 1133 | url = "https://github.com/NixOS/nixpkgs-channels/archive/nixos-20.03.tar.gz"; 1134 | }) {}; 1135 | 1136 | in { 1137 | 1138 | network = { 1139 | inherit pkgs; 1140 | description = "My NixOS server"; 1141 | }; 1142 | 1143 | "159.69.155.132" = { config, pkgs, ... }: { 1144 | 1145 | ... 1146 | 1147 | networking = { 1148 | domain = "demo.romainviallard.dev"; 1149 | firewall.allowedTCPPorts = [ 80 443 ]; 1150 | }; 1151 | 1152 | security.acme = { 1153 | acceptTerms = true; 1154 | # validMinDays = 999; 1155 | # server = "https://acme-staging-v02.api.letsencrypt.org/directory"; # uncomment this to use the staging server 1156 | email = "romain@romainviallard.dev"; 1157 | certs."demo.romainviallard.dev".extraDomains = { 1158 | # "demo2.romainviallard.dev" = null; 1159 | }; 1160 | }; 1161 | 1162 | ... 1163 | 1164 | services.nginx = { 1165 | enable = true; 1166 | 1167 | recommendedGzipSettings = true; 1168 | recommendedOptimisation = true; 1169 | recommendedProxySettings = true; 1170 | recommendedTlsSettings = true; 1171 | 1172 | # https://nixos.org/nixos/manual/#module-security-acme-nginx 1173 | virtualHosts = { 1174 | 1175 | "demo.romainviallard.dev" = { 1176 | enableACME = true; 1177 | forceSSL = true; 1178 | locations."/" = { 1179 | proxyPass = "http://127.0.0.1:${toString config.services.myapp.port}"; 1180 | }; 1181 | }; 1182 | 1183 | # "demo2.romainviallard.dev" = { 1184 | # useACMEHost = "demo.romainviallard.dev"; 1185 | # forceSSL = true; 1186 | # locations."/" = { 1187 | # proxyPass = "http://127.0.0.1:${toString config.services.myapp.port}"; 1188 | # }; 1189 | # }; 1190 | }; 1191 | }; 1192 | 1193 | }; 1194 | } 1195 | ~~~ 1196 | We've opened the 80 and 443 ports on our firewall in order to let web users access our app. 1197 | As you can see, we directly reference the configuration value **config.services.myapp.port** here even if we are out of our module definition file. 1198 | Here, we leverage Let's Encrypt to generate an ssl certificate for the **demo.romainviallard.dev** domain. 1199 | To generate a single certificate for multiple domains, you assign **extraDomains** to your main domain, I didn't do it here since I haven't configured a dns entry for the **demo2.romainviallard.dev** subdomain and this prevents the Let's encrypt process from succeeding. 1200 | We then enable the **Nginx** service using its recommended settings and we add a virtual host which forwards all requests to our internal app server. 1201 | The important attribute here is **enableACME** which is what you must use with **true** on your main domain (the one the certificate will primarily generated for). In case you have several domains using the same certificate, you must use the **useACMEHost** instead of **enableACME** attribute on secondary domains, giving it the primary domain. 1202 | The **security.acme.validMinDays** attribute can be uncommented when you need to force a refresh of the certificate, in case you've added a domain to it for example. 1203 | The **security.acme.server** attribute can be used to provide a different letsencrypt server, when not provided, it uses the production one. You should use a staging server when making configuration tests as to not be throttled by Let's Encrypt because of too many requests. 1204 | 1205 | Let's check this still works. 1206 | ~~~bash 1207 | [romain@clevo-N141ZU:~/Code/deploy-app-with-nixos]$ curl https://demo.romainviallard.dev/ 1208 | Current redis value: xxxxxxxxxxx 1209 | Current sql value: 11 1210 | [romain@clevo-N141ZU:~/Code/deploy-app-with-nixos]$ curl https://demo.romainviallard.dev/ 1211 | Current redis value: xxxxxxxxxxxx 1212 | Current sql value: 12 1213 | ~~~ 1214 | 1215 | All is fine ! 1216 | 1217 | Alright, that's it for today, I hope this blog post has given you a nice overview of the niceties the Nix ecosystem brings to the table. As usual, if you have any questions, feel free open an issue. 1218 | --------------------------------------------------------------------------------