├── .gitignore ├── part1 ├── shell.nix ├── image.nix └── network.nix ├── part2 ├── versions.tf ├── common.nix ├── shell.nix ├── .terraform.lock.hcl ├── parsetf.nix ├── main.tf └── network.nix ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | result 2 | show.json 3 | .terraform/ 4 | -------------------------------------------------------------------------------- /part1/shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { } }: 2 | pkgs.mkShell { 3 | buildInputs = with pkgs; [ curl morph ]; 4 | } 5 | -------------------------------------------------------------------------------- /part2/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | digitalocean = { 4 | source = "digitalocean/digitalocean" 5 | version = "2.2.0" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /part1/image.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { } }: 2 | let config = { 3 | imports = [ ]; 4 | }; 5 | in 6 | (pkgs.nixos config).digitalOceanImage 7 | -------------------------------------------------------------------------------- /part2/common.nix: -------------------------------------------------------------------------------- 1 | { modulesPath, lib, ... }: 2 | { 3 | imports = lib.optional (builtins.pathExists ./do-userdata.nix) ./do-userdata.nix ++ [ 4 | (modulesPath + "/virtualisation/digital-ocean-config.nix") 5 | ]; 6 | 7 | deployment.targetUser = "root"; 8 | system.stateVersion = "21.11"; 9 | } 10 | -------------------------------------------------------------------------------- /part2/shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { } }: 2 | let 3 | myTerraform = pkgs.terraform.withPlugins (tp: [ tp.digitalocean ]); 4 | ter = pkgs.writeShellScriptBin "ter" '' 5 | terraform $@ && terraform show -json > show.json 6 | ''; 7 | in 8 | pkgs.mkShell { 9 | buildInputs = with pkgs; [ curl jq morph myTerraform ter ]; 10 | } 11 | -------------------------------------------------------------------------------- /part2/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/digitalocean/digitalocean" { 5 | version = "2.2.0" 6 | constraints = "2.2.0" 7 | hashes = [ 8 | "h1:xyVnhQdSY0K0p9E2A5vJ6iOpV9cfWJuZIvOQCS0TyZo=", 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /part2/parsetf.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { } 2 | , lib ? pkgs.lib 3 | }: 4 | let 5 | resourcesInModule = type: module: 6 | builtins.filter (r: r.type == type) module.resources ++ 7 | lib.flatten (map (resourcesInModule type) (module.child_modules or [ ])); 8 | resourcesByType = type: resourcesInModule type payload.values.root_module; 9 | payload = builtins.fromJSON (builtins.readFile ./show.json); 10 | in 11 | { 12 | inherit resourcesByType; 13 | } 14 | -------------------------------------------------------------------------------- /part2/main.tf: -------------------------------------------------------------------------------- 1 | provider "digitalocean" {} 2 | 3 | resource "digitalocean_droplet" "backend" { 4 | name = "backend${count.index + 1}" 5 | region = "ams3" 6 | size = "s-1vcpu-1gb" 7 | image = 75674995 8 | ssh_keys = [27010799] 9 | 10 | count = 3 11 | } 12 | 13 | resource "digitalocean_droplet" "loadbalancer" { 14 | name = "loadbalancer${count.index + 1}" 15 | region = "ams3" 16 | size = "s-1vcpu-1gb" 17 | image = 75674995 18 | ssh_keys = [27010799] 19 | 20 | count = 2 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A cookbook for deploying NixOS to cloud servers 2 | 3 | * [Part 1](/part1) provides a way to deploy NixOS 4 | to [DigitalOcean](https://www.digitalocean.com/) 5 | by building a custom NixOS disk image. 6 | It also introduces remote management of your cloud servers 7 | using [Morph](https://github.com/DBCDK/morph) 8 | and uses this to deploy an instance of nginx HTTP server. 9 | See also 10 | [the accompanying article](https://justinas.org/nixos-in-the-cloud-step-by-step-part-1). 11 | * [Part 2](/part2) shows how to spawn NixOS machines in bulk 12 | using Terraform, as well as how to use Terraform state 13 | in Nix expressions, in order to automatically deploy 14 | to the managed infrastructure. 15 | See also 16 | [the accompanying article](https://justinas.org/nixos-in-the-cloud-step-by-step-part-2). 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Justinas Stankevicius 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /part1/network.nix: -------------------------------------------------------------------------------- 1 | { 2 | network = { 3 | pkgs = import 4 | (builtins.fetchGit { 5 | name = "nixos-21.11-2021-12-19"; 6 | url = "https://github.com/NixOS/nixpkgs"; 7 | ref = "refs/heads/nixos-21.11"; 8 | rev = "e6377ff35544226392b49fa2cf05590f9f0c4b43"; 9 | }) 10 | { }; 11 | }; 12 | 13 | nixie = { modulesPath, lib, name, ... }: { 14 | imports = lib.optional (builtins.pathExists ./do-userdata.nix) ./do-userdata.nix ++ [ 15 | (modulesPath + "/virtualisation/digital-ocean-config.nix") 16 | ]; 17 | 18 | deployment.targetHost = "198.51.100.207"; 19 | deployment.targetUser = "root"; 20 | 21 | networking.hostName = name; 22 | 23 | system.stateVersion = "21.11"; # Do not change lightly! 24 | 25 | deployment.healthChecks = { 26 | http = [ 27 | { 28 | scheme = "http"; 29 | port = 80; 30 | path = "/"; 31 | description = "check that nginx is running"; 32 | } 33 | ]; 34 | }; 35 | 36 | networking.firewall.allowedTCPPorts = [ 80 ]; 37 | 38 | services.nginx = { 39 | enable = true; 40 | virtualHosts.default = { 41 | default = true; 42 | locations."/".return = "200 \"Hello from Nixie!\""; 43 | }; 44 | }; 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /part2/network.nix: -------------------------------------------------------------------------------- 1 | let 2 | resourcesByType = (import ./parsetf.nix { }).resourcesByType; 3 | 4 | droplets = resourcesByType "digitalocean_droplet"; 5 | backends = builtins.filter (d: d.name == "backend") droplets; 6 | loadbalancers = builtins.filter (d: d.name == "loadbalancer") droplets; 7 | 8 | mkBackend = resource: { name, ... }: { 9 | imports = [ ./common.nix ]; 10 | 11 | deployment.targetHost = resource.values.ipv4_address; 12 | networking.hostName = resource.values.name; 13 | 14 | networking.firewall.interfaces.ens4.allowedTCPPorts = [ 80 ]; 15 | services.nginx = { 16 | enable = true; 17 | virtualHosts.default = { 18 | default = true; 19 | locations."/".return = "200 \"Hello from ${name} at ${resource.values.ipv4_address}\""; 20 | }; 21 | }; 22 | }; 23 | 24 | mkLoadBalancer = resource: { name, ... }: { 25 | imports = [ ./common.nix ]; 26 | 27 | deployment.targetHost = resource.values.ipv4_address; 28 | networking.hostName = resource.values.name; 29 | 30 | networking.firewall.allowedTCPPorts = [ 80 ]; 31 | services.nginx = { 32 | enable = true; 33 | upstreams.backend.servers = builtins.listToAttrs 34 | (map (r: { name = r.values.ipv4_address_private; value = { }; }) 35 | backends); 36 | virtualHosts.default = { 37 | default = true; 38 | locations."/".proxyPass = "http://backend"; 39 | }; 40 | }; 41 | }; 42 | in 43 | { 44 | network = { 45 | pkgs = import 46 | (builtins.fetchGit { 47 | name = "nixos-21.11-2021-12-19"; 48 | url = "https://github.com/NixOS/nixpkgs"; 49 | ref = "refs/heads/nixos-21.11"; 50 | rev = "e6377ff35544226392b49fa2cf05590f9f0c4b43"; 51 | }) 52 | { }; 53 | }; 54 | } // 55 | builtins.listToAttrs (map (r: { name = r.values.name; value = mkBackend r; }) backends) // 56 | builtins.listToAttrs (map (r: { name = r.values.name; value = mkLoadBalancer r; }) loadbalancers) 57 | --------------------------------------------------------------------------------