├── README.md ├── container.nix ├── default.nix └── nix-services ├── default.nix ├── environment.nix ├── supervisord.nix ├── systemd.nix └── user.nix /README.md: -------------------------------------------------------------------------------- 1 | ========== 2 | nix-rehash 3 | ========== 4 | 5 | 6 | Nix development utils that will blow up your mind 7 | 8 | 9 | reService - fullstack@dev 10 | -------------------------- 11 | 12 | reService takes your nixos config of services and creates user-enabled supervisord 13 | config that you can use for development or deployment on non nixos systems. 14 | Having deterministic fullstack in development has never been more awesome. 15 | 16 | - Create `default.nix` 17 | 18 | ``` 19 | { pkgs ? import {} 20 | , projectName ? "myProject" 21 | , nix-rehash ? import }: 22 | with pkgs.lib; 23 | with pkgs; 24 | 25 | let 26 | services = nix-rehash.reService { 27 | name = "${projectName}"; 28 | configuration = let servicePrefix = "/tmp/${projectName}/services"; in [ 29 | ({ config, pkgs, ...}: { 30 | services.postgresql.enable = true; 31 | services.postgresql.package = pkgs.postgresql92; 32 | services.postgresql.dataDir = "${servicePrefix}/postgresql"; 33 | }) 34 | ]; 35 | }; 36 | in myEnvFun { 37 | name = projectName; 38 | buildInputs = [ services ]; 39 | } 40 | ``` 41 | 42 | - install `nix-env -f default.nix -i` 43 | - load environemnt `load-env-myProject` 44 | - start services `myProject-start-services`, control services `myProject-control-services`, 45 | stop services `myProject-stop-services` 46 | 47 | Now build this with hydra and pass the environment around :) 48 | 49 | Alternative using nix-shell: 50 | 51 | - set `buildInputs = [ services.config.supervisord.bin ];` 52 | - run `nix-shell` 53 | - use `supervisord` and `supervisorctl` as you wish 54 | 55 | reContain - heroku@home 56 | ----------------------- 57 | 58 | reContain makes nixos enabled installable container that can auto-update 59 | itself. Now you can build container on hydra and auto update it on 60 | host machine. Staging or deployments have never been easier :) 61 | 62 | - Create `default.nix` 63 | 64 | ``` 65 | { pkgs ? import 66 | , name ? "myProject" 67 | , nix-rehash ? import }: 68 | with pkgs.lib; 69 | with pkgs; 70 | 71 | { 72 | container = nix-rehash.reContain { 73 | inherit name; 74 | configuration = [{ 75 | services.openssh.enable = true; 76 | services.openssh.ports = [ 25 ]; 77 | users.extraUsers.root.openssh.authorizedKeys.keys = [ (builtins.readFile ./id_rsa.pub) ]; 78 | }]; 79 | }; 80 | } 81 | ``` 82 | - do `nix-env [-f default.nix] -i myProject-container` or build with hydra and add a channel 83 | - start container: `sudo myProject-start-container` 84 | - ssh to container: `ssh localhost -p 25` 85 | - enable auto updates with cron: 86 | ``` 87 | * * * * * nix-env -i myProject-container && sudo $HOME/.nix-profile/bin/myProject-update-container 88 | ``` 89 | - stop container: `sudo myProject-stop-container` 90 | -------------------------------------------------------------------------------- /container.nix: -------------------------------------------------------------------------------- 1 | { system ? builtins.currentSystem 2 | , pkgs ? import { inherit system; } 3 | , lxcExtraConfig ? "" 4 | , name 5 | , configuration 6 | }: 7 | with pkgs; 8 | with pkgs.lib; 9 | 10 | let 11 | container_root = "/var/lib/containers/${name}"; 12 | 13 | switchScript = writeScript "${name}-switch" '' 14 | #!${pkgs.bash}/bin/bash 15 | old_gen="$(readlink -f /old_config)" 16 | new_gen="$(readlink -f /new_config)" 17 | [ "x$old_gen" != "x$new_gen" ] || exit 0 18 | $new_gen/bin/switch-to-configuration switch 19 | rm /old_config && ln -fs $new_gen /old_config 20 | ''; 21 | 22 | container = (import { 23 | modules = 24 | let extraConfig = { 25 | nixpkgs.system = system; 26 | boot.isContainer = true; 27 | networking.hostName = mkDefault name; 28 | security.pam.services.sshd.startSession = mkOverride 50 false; 29 | services.cron.systemCronJobs = [ 30 | "* * * * * root ${switchScript} > /var/log/switch-config.log 2>&1" 31 | ]; 32 | }; 33 | in [ extraConfig ] ++ configuration; 34 | prefix = [ "systemd" "containers" name ]; 35 | }).config.system.build.toplevel; 36 | 37 | containerConfig = '' 38 | lxc.utsname = ${name} 39 | lxc.arch = ${if system == "x86_64-linux" then "x86_64" else "i686"} 40 | lxc.tty = 6 41 | lxc.pts = 1024 42 | 43 | ## Capabilities 44 | lxc.cap.drop = audit_control audit_write mac_admin mac_override mknod setfcap 45 | lxc.cap.drop = sys_boot sys_module sys_pacct sys_rawio sys_time 46 | 47 | ## Devices 48 | lxc.cgroup.devices.deny = a # Deny access to all devices 49 | 50 | # Allow to mknod all devices (but not using them) 51 | lxc.cgroup.devices.allow = c *:* m 52 | lxc.cgroup.devices.allow = b *:* m 53 | 54 | lxc.cgroup.devices.allow = c 1:3 rwm 55 | lxc.cgroup.devices.allow = c 1:5 rwm 56 | lxc.cgroup.devices.allow = c 1:8 rwm 57 | lxc.cgroup.devices.allow = c 1:9 rwm 58 | lxc.cgroup.devices.allow = c 4:0 rwm 59 | lxc.cgroup.devices.allow = c 4:1 rwm 60 | lxc.cgroup.devices.allow = c 4:2 rwm 61 | lxc.cgroup.devices.allow = c 4:3 rwm 62 | lxc.cgroup.devices.allow = c 5:0 rwm 63 | lxc.cgroup.devices.allow = c 5:1 rwm 64 | lxc.cgroup.devices.allow = c 5:2 rwm 65 | lxc.cgroup.devices.allow = c 10:229 rwm 66 | lxc.cgroup.devices.allow = c 136:* rwm 67 | lxc.cgroup.devices.allow = c 254:0 rwm 68 | 69 | ## Mounts 70 | lxc.mount.entry = /nix/store nix/store none defaults,bind.ro 0.0 71 | lxc.autodev = 1 72 | 73 | ${lxcExtraConfig} 74 | 75 | ## Network 76 | lxc.network.type = veth 77 | lxc.network.name = eth0 78 | lxc.network.flags = up 79 | 80 | ''; 81 | 82 | startContainer = writeTextFile { 83 | name = "${name}-container-start"; 84 | text = '' 85 | #!${pkgs.bash}/bin/bash 86 | if [ -z "$CONTAINER_ROOT" ]; then 87 | export CONTAINER_ROOT="/var/lib/containers/${name}" 88 | fi 89 | echo "Using $CONTAINER_ROOT as rootfs" 90 | mkdir -p $CONTAINER_ROOT/{proc,sys,dev,nix/store,etc} 91 | mkdir -p /var/lib/lxc/rootfs 92 | chmod 0755 $CONTAINER_ROOT/etc 93 | if ! [ -e $CONTAINER_ROOT/etc/os-release ]; then 94 | touch $CONTAINER_ROOT/etc/os-release 95 | fi 96 | rm $CONTAINER_ROOT/old_config 97 | ln -fs "${container}" $CONTAINER_ROOT/old_config 98 | rm $CONTAINER_ROOT/new_config 99 | ln -fs "${container}" $CONTAINER_ROOT/new_config 100 | ${pkgs.lxc}/bin/lxc-start -n "${name}" \ 101 | -f "${pkgs.writeText "container.conf" containerConfig}" \ 102 | -s lxc.rootfs=$CONTAINER_ROOT \ 103 | "$@" "${container}/init" 104 | ''; 105 | executable = true; 106 | destination = "/bin/${name}-container-start"; 107 | }; 108 | 109 | stopContainer = writeTextFile { 110 | name = "${name}-container-stop"; 111 | text = '' 112 | #!${pkgs.bash}/bin/bash 113 | ${pkgs.lxc}/bin/lxc-stop -n "${name}" "$@" 114 | ''; 115 | executable = true; 116 | destination = "/bin/${name}-container-stop"; 117 | }; 118 | 119 | updateContainer = writeTextFile { 120 | name = "${name}-container-update"; 121 | text = '' 122 | #!${pkgs.bash}/bin/bash 123 | if [ -z "$CONTAINER_ROOT" ]; then 124 | export CONTAINER_ROOT="/var/lib/containers/${name}" 125 | fi 126 | rm $CONTAINER_ROOT/new_config 127 | ln -fs ${container} $CONTAINER_ROOT/new_config 128 | ''; 129 | executable = true; 130 | destination = "/bin/${name}-container-update"; 131 | }; 132 | in buildEnv { 133 | name = "${name}-container"; 134 | paths = [ startContainer stopContainer updateContainer ]; 135 | } 136 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { 2 | reService = import ./nix-services; 3 | reContain = import ./container.nix; 4 | } 5 | -------------------------------------------------------------------------------- /nix-services/default.nix: -------------------------------------------------------------------------------- 1 | { system ? builtins.currentSystem 2 | , pkgs ? import { 3 | inherit system; 4 | # Darwin needs a few packages overrides 5 | config = if system == "x86_64-darwin" then { 6 | packageOverrides = pkgs: { 7 | shadow = pkgs.stdenv.mkDerivation { 8 | name="shadow"; outputs=["out" "su"]; 9 | buildCommand="touch $out; touch $su"; 10 | }; 11 | }; 12 | } else {}; 13 | } 14 | , name 15 | , configuration 16 | }: 17 | with pkgs.lib; 18 | 19 | let 20 | moduleList = [ 21 | ./user.nix ./supervisord.nix ./systemd.nix ./environment.nix 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | # 42 | ]; 43 | 44 | config = (evalModules { 45 | modules = configuration ++ moduleList; 46 | args = { inherit pkgs; }; 47 | }).config; 48 | 49 | systemd = import ./systemd.nix { inherit pkgs config; }; 50 | 51 | stopServices = pkgs.writeScript "stopServices" '' 52 | #!${pkgs.stdenv.shell} 53 | ${config.supervisord.bin}/bin/supervisorctl shutdown 54 | ''; 55 | 56 | servicesControl = pkgs.stdenv.mkDerivation { 57 | name = "${name}-servicesControl"; 58 | src = ./.; 59 | 60 | phases = [ "installPhase" ]; 61 | 62 | installPhase = '' 63 | mkdir -p $out/bin/ 64 | ln -s ${config.supervisord.bin}/bin/supervisord $out/bin/${name}-start-services 65 | ln -s ${stopServices} $out/bin/${name}-stop-services 66 | ln -s ${config.supervisord.bin}/bin/supervisorctl $out/bin/${name}-control-services 67 | ''; 68 | 69 | passthru.config = config; 70 | }; 71 | 72 | in pkgs.buildEnv { 73 | name = "${name}-services"; 74 | paths = [ servicesControl ] ++ config.environment.systemPackages; 75 | 76 | inherit (servicesControl) passthru; 77 | } 78 | -------------------------------------------------------------------------------- /nix-services/environment.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, ... }: 2 | with pkgs.lib; 3 | { 4 | options = { 5 | environment.systemPackages = mkOption { 6 | default = []; 7 | description = "Packages to be put in the system profile."; 8 | }; 9 | 10 | environment.umask = mkOption { 11 | default = "002"; 12 | type = with types; string; 13 | }; 14 | 15 | # HACK HACK 16 | system.activationScripts.etc = mkOption {}; # Ignore 17 | system.build.etc = mkOption {}; # Ignore 18 | environment.etc = mkOption {}; # Ignore 19 | environment.sessionVariables = mkOption {}; # Ignore 20 | 21 | }; 22 | 23 | config = { 24 | environment.systemPackages = with pkgs; [ ]; 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /nix-services/supervisord.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, ... }: 2 | with pkgs.lib; 3 | let 4 | serviceOpts = { name, config, ...}: { 5 | options = { 6 | command = mkOption { 7 | description = "The command to execute"; 8 | }; 9 | directory = mkOption { 10 | default = "/"; 11 | description = "Current directory when running the command"; 12 | }; 13 | environment = mkOption { 14 | default = {}; 15 | example = { 16 | PATH = "/some/path"; 17 | }; 18 | }; 19 | path = mkOption { 20 | default = []; 21 | description = "Current directory when running the command"; 22 | }; 23 | stopsignal = mkOption { 24 | default = "TERM"; 25 | }; 26 | startsecs = mkOption { 27 | default = 1; 28 | example = 0; 29 | }; 30 | pidfile = mkOption { 31 | default = null; 32 | }; 33 | }; 34 | }; 35 | 36 | services = config.supervisord.services; 37 | 38 | supervisor = config.supervisord.package; 39 | 40 | supervisordWrapper = pkgs.writeScript "supervisord-wrapper" '' 41 | #!${pkgs.stdenv.shell} 42 | extraFlags="" 43 | if [ -n "$STATEDIR" ]; then 44 | extraFlags="-j $STATEDIR/run/supervisord.pid -d $STATEDIR -q $STATEDIR/log/ -l $STATEDIR/log/supervisord.log" 45 | mkdir -p "$STATEDIR"/{run,log} 46 | else 47 | mkdir -p "${config.supervisord.stateDir}"/{run,log} 48 | fi 49 | 50 | export PATH="${pkgs.coreutils}/bin" 51 | 52 | # Run start scripts first 53 | "${config.userNix.startScript}" 54 | 55 | # Run supervisord 56 | exec ${supervisor}/bin/supervisord -c "${config.supervisord.configFile}" $extraFlags "$@" 57 | ''; 58 | 59 | supervisorctlWrapper = pkgs.writeScript "supervisorctl-wrapper" '' 60 | #!${pkgs.stdenv.shell} 61 | exec ${supervisor}/bin/supervisorctl -c "${config.supervisord.configFile}" "$@" 62 | ''; 63 | 64 | in { 65 | options = { 66 | supervisord = { 67 | enable = mkOption { 68 | default = true; 69 | type = types.bool; 70 | }; 71 | 72 | port = mkOption { 73 | default = 65123; 74 | type = types.int; 75 | }; 76 | 77 | package = mkOption { 78 | default = pkgs.pythonPackages.supervisor; 79 | type = types.package; 80 | description = '' 81 | Supervisord package to use. 82 | ''; 83 | }; 84 | 85 | services = mkOption { 86 | default = {}; 87 | type = types.loaOf types.optionSet; 88 | description = '' 89 | Supervisord services to start. 90 | ''; 91 | options = [ serviceOpts ]; 92 | }; 93 | 94 | stateDir = mkOption { 95 | default = "./var"; 96 | type = types.str; 97 | description = '' 98 | Supervisord state directory. 99 | ''; 100 | }; 101 | 102 | tailLogs = mkOption { 103 | default = false; 104 | type = types.bool; 105 | description = '' 106 | Whether or not to tail all logs to standard out. 107 | ''; 108 | }; 109 | 110 | configFile = mkOption { 111 | internal = true; 112 | }; 113 | 114 | bin = mkOption { 115 | internal = true; 116 | }; 117 | }; 118 | }; 119 | 120 | config = mkIf config.supervisord.enable { 121 | supervisord.configFile = pkgs.writeText "supervisord.conf" '' 122 | [supervisord] 123 | pidfile=${config.supervisord.stateDir}/run/supervisord.pid 124 | childlogdir=${config.supervisord.stateDir}/log/ 125 | logfile=${config.supervisord.stateDir}/log/supervisord.log 126 | 127 | [supervisorctl] 128 | serverurl = http://localhost:${toString config.supervisord.port} 129 | 130 | [inet_http_server] 131 | port = 127.0.0.1:${toString config.supervisord.port} 132 | 133 | [rpcinterface:supervisor] 134 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 135 | 136 | ${concatMapStrings (name: 137 | let 138 | cfg = getAttr name services; 139 | path = if isList cfg.path then concatStringsSep ":" cfg.path else cfg.path; 140 | in 141 | '' 142 | [program:${name}] 143 | command=${if cfg.pidfile == null then cfg.command else "${supervisor}/bin/pidproxy ${cfg.pidfile} ${cfg.command}"} 144 | environment=${concatStrings 145 | (mapAttrsToList (name: value: "${name}=\"${value}\",") ( 146 | cfg.environment // { PATH = concatStringsSep ":" 147 | [("%(ENV_PATH)s") (path) (maybeAttr "PATH" "" cfg.environment)]; 148 | } 149 | ) 150 | )} 151 | directory=${cfg.directory} 152 | redirect_stderr=true 153 | startsecs=${toString cfg.startsecs} 154 | stopsignal=${cfg.stopsignal} 155 | stopasgroup=true 156 | '' 157 | ) (attrNames services) 158 | } 159 | ''; 160 | 161 | supervisord.bin = pkgs.stdenv.mkDerivation { 162 | name = "${supervisor.name}-wrapper"; 163 | 164 | phases = [ "installPhase" ]; 165 | 166 | installPhase = '' 167 | mkdir -p $out/bin/ 168 | cp ${supervisordWrapper} $out/bin/supervisord 169 | cp ${supervisorctlWrapper} $out/bin/supervisorctl 170 | ''; 171 | 172 | }; 173 | 174 | }; 175 | } 176 | 177 | -------------------------------------------------------------------------------- /nix-services/systemd.nix: -------------------------------------------------------------------------------- 1 | { pkgs, config, ... }: 2 | with pkgs.lib; 3 | with import { 4 | inherit config; 5 | inherit (pkgs) lib; 6 | }; 7 | let 8 | services = config.systemd.services; 9 | 10 | isOneShot = cfg: hasAttr "Type" cfg.serviceConfig && cfg.serviceConfig.Type == "oneshot"; 11 | 12 | runServices = filterAttrs (name: cfg: !(isOneShot cfg)) services; 13 | 14 | oneShotServices = filterAttrs (name: cfg: isOneShot cfg) services; 15 | 16 | filterCommand = cmd: 17 | let 18 | filtered = substring 1 (stringLength cmd -2) cmd; 19 | splitted = pkgs.lib.splitString (" " + substring 0 0 filtered) filtered; 20 | in if eqStrings (substring 0 1 cmd) "@" then 21 | traceVal (head splitted) + concatStringsSep " " (drop 2 splitted) 22 | else cmd; 23 | 24 | configToCommand = name: cfg: '' 25 | #!/bin/sh -e 26 | ${if hasAttr "preStart" cfg then cfg.preStart else ""} 27 | ${if hasAttr "ExecStart" cfg.serviceConfig then 28 | filterCommand cfg.serviceConfig.ExecStart 29 | else if hasAttr "script" cfg then 30 | (pkgs.writeScript "${name}-start.sh" cfg.script) 31 | else 32 | "echo" 33 | } & 34 | export MAINPID=$! 35 | ${if hasAttr "postStart" cfg then cfg.postStart else ""} 36 | wait 37 | ''; 38 | 39 | in { 40 | 41 | options = { 42 | systemd.services = mkOption { 43 | default = {}; 44 | type = types.attrsOf types.optionSet; 45 | options = [ serviceOptions ]; 46 | }; # TODO make more specific 47 | 48 | systemd.globalEnvironment = mkOption { 49 | default = {}; 50 | }; 51 | 52 | services.dataPrefix = mkOption { 53 | default = "/var"; 54 | type = types.path; 55 | description = ''''; 56 | }; 57 | }; 58 | 59 | config = { 60 | userNix.startScripts."1-systemd-oneshot" = concatMapStrings (name: "${configToCommand name (getAttr name oneShotServices)}\n") (attrNames oneShotServices); 61 | 62 | supervisord.services = listToAttrs (map (name: 63 | let 64 | cfg = getAttr name services; 65 | in 66 | { 67 | name = name; 68 | value = { 69 | command = pkgs.writeScript "${name}-run" (configToCommand name cfg); 70 | environment = cfg.environment // config.systemd.globalEnvironment; 71 | path = cfg.path; 72 | stopsignal = if hasAttr "KillSignal" cfg.serviceConfig then 73 | substring 3 (stringLength cfg.serviceConfig.KillSignal) cfg.serviceConfig.KillSignal 74 | else "TERM"; 75 | pidfile = if hasAttr "PIDFile" cfg.serviceConfig then cfg.serviceConfig.PIDFile else null; 76 | }; 77 | } 78 | ) (attrNames (filterAttrs (n: v: v.enable) runServices))); 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /nix-services/user.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, ... }: 2 | with pkgs.lib; 3 | { 4 | options = { 5 | userNix.startScripts = mkOption { 6 | default = {}; 7 | description = "Scripts (as text) to be run during build, executed alphabetically"; 8 | }; 9 | 10 | userNix.startScript = mkOption {}; 11 | 12 | }; 13 | 14 | config = { 15 | userNix.startScript = concatStrings (attrValues config.userNix.startScripts); 16 | }; 17 | } 18 | --------------------------------------------------------------------------------