I’ve had fun recently with making server Ubuntu behave more like NixOS. I’ll share some parts, which work for me, and I hope people can suggest improvements (drop Ubuntu? haha, nice try).
So, my story is based around Nix, overlays, home-manager and declarative containers. Let’s start with required installations
I’ll post installation instructions as Salt state config, but you can adapt to whatever you like.
Nix
# TODO: set secure_path in /etc/sudoers to Nix-enabled value
# In the meanwhile `sudo -i` works
install-nix:
cmd.run:
# will install as root internally
- user: ubuntu
- name: |
curl -o install.sh https://nixos.org/nix/install
yes | sh ./install.sh --daemon
echo "source /etc/profile.d/nix.sh" | sudo tee -a /etc/bash.bashrc
- unless: bash -c 'nix-env --version'
That’s it, we have Nix with daemon installed!
nix-collect-garbage:
cron.present:
- name: "/root/.nix-profile/bin/nix-collect-garbage -d && /root/.nix-profile/bin/nix-store --optimise"
- user: root
- special: '@daily'
We are also not very interested in rollbacks, as our approach is very hacky. But you may do GC manually if you want
Overlay
I’ll use 18.09 home-manager release, but you can pinpoint it to specific version.
# This is central place to handle nixpkgs checkouts
self: super: let
pkgs = self;
home-manager_checkout = builtins.fetchGit {
name = "amplicare-home-manager-18.09";
url = https://github.com/rycee/home-manager;
ref = "release-18.09";
};
in {
_home_checkout = pkgs.runCommand "checkout" {} ''
cp -r ${home-manager_checkout} $out/
'';
_home = import home-manager_checkout { };
}
Then you have to install this file into ~/.config/nixpkgs/overlays/
and to /root/.config/nixpkgs/overlays/
. Why root’s? Just to make it consistent nix-build
and sudo -i nix-build
home-manager install
install-home:
cmd.run:
- user: {{user}}
- name: |
source /etc/profile.d/nix.sh
sudo -i nix-build '<nixpkgs>' -A _home_checkout \
-o /nix/var/nix/profiles/per-user/$USER/channels/home-manager
nix-shell '<nixpkgs>' -A _home.install
- unless: bash -c 'home-manager generations | grep -q id'
- require:
- install-nix
- file: root/.config/nixpkgs/overlays/pkgs-overlay.nix
- file: {{user_home}}/.config/nixpkgs/overlays/pkgs-overlay.nix
And here we have home-manager installed! Note that it will use home channel from git checkout only during installation, no auto-channel update specified here. Probably a TODO
Personally, I’ve installed it as root, so I have full control over system from home manager, but unfortunately I can’t do declarative /etc
update, because home-manager is about HOME mgmt… Oh well, maybe smbd can suggest how to fix this?
Home-manager
This is home-manager config skeleton, which is tuned to allow working with user systemd services - lingering and controlling:
{ config, pkgs, ... }: let
dag = config.lib.dag;
in {
programs.home-manager.enable = true;
home.stateVersion = "18.09";
home.file.".profile".text = ''
# -*- mode: sh -*-
if [ "$BASH" ]; then
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
fi
mesg n || true
# this one actually makes envvars from home-manager to be set on shell login
. "${config.home.profileDirectory}/etc/profile.d/hm-session-vars.sh"
'';
home.language.base = "C";
home.sessionVariables.XDG_RUNTIME_DIR = "/run/user/$UID"; # for systemctl --user
# so user systemd services are not stopped on logout
home.activation.ensureDirs = dag.entryBefore ["reloadSystemD"] ''
loginctl enable-linger $USER
'';
# We'll setup container later, but this is required if you want containers to start on boot
home.activation.startContainers = dag.entryBefore ["reloadSystemD"] ''
machines_wants=/etc/systemd/system/machines.target.wants
sudo mkdir -p $machines_wants
sudo rm -rf $machines_wants/*
for service in $(ls -d /usr/lib/systemd/system/*); do
sudo ln -s $service $machines_wants/
done
'';
# TODO: this isn't restarted automatically on changes!!!
# systemd.user.services.nomad-agent = ...
}
I’m still not sure why user services are not restarted on configuration changes. Maybe someone can help me here?
Containers
Containers are awesome, in sense that you get full NixOS configuration, no need to do wrappers around services and so on, They are costly to start though, but not much. So, prerequisites first:
/etc/static/os-release:
file.managed:
- user: root
- group: root
- mode: 755
- makedirs: True
- contents: |
dummy
# Machinectl and systemd-nspawn, which are required for nixos containers
systemd-container:
pkg:
- installed
The first one fixes bug in NixOS containers, which require /etc/static/os-release
to exist. Second installs container tools. Maybe I could go with installing Nixpkgs systemd instead, but I don’t know how would it interop with default Ubuntu’s systemd.
Overlay
self: super: let
pkgs = self;
in {
extra-container = pkgs.stdenv.mkDerivation {
name = "extra-container-patched";
src = pkgs.fetchurl {
url = "https://github.com/erikarvstedt/extra-container/archive/0.2.tar.gz";
sha256 = "1gz7rl2hx8kbmy5wdyai75mbscr0v61da4h7paz3j3yzfk6p7wf9";
};
buildCommand = ''
unpackPhase && cd "$sourceRoot"
# extra-container was designed for NixOS. We have to adapt it for Ubuntu
sed -i 's|/etc/systemd-mutable|/usr/lib/systemd|g' extra-container
mkdir -p $out/bin
cp extra-container $out/bin/extra-container
patchShebangs $out/bin
'';
};
}
and home.nix change:
home.packages = with pkgs; [
extra-container
nixos-container
];
This brings extra-container
tool, with a fix for Ubuntu.
Activation
Finally, we’ll set activate
phase, which will be called on every deploy. If you don’t use salt, then this should be a script activate.sh
:
activate:
cmd.run:
- user: root
- name: |
set -e
source /etc/profile.d/nix.sh
rm ~/.profile || true # home-manager barfs this file exists already
home-manager switch
extra-container create --start --restart /var/www/containers.nix
# TODO: remove when extra-container learns to treat autoStart in containers correctly!!!
home-manager switch
- require:
- file: /root/.config/nixpkgs/home.nix
- file: /var/www/containers.nix
Unfortunately, I have to call home-manager switch
twice, because extra-container tool is installed via home-manager, and containers autostart is managed by home-manager… Chicken and egg, so I call both.
Containers configuration
The /var/www/containers.nix
is an actual NixOS configuration, with declarative containers defined. extra-container
evaluates it just like regular NixOS, but manages only containers. My real config is complicated one, so I’ll show some simple example:
let
common = {
nixpkgs.overlays = [
(import ./overlay.nix) # ideally all overlays you use
];
imports = [
<nixpkgs/nixos/modules/profiles/minimal.nix>
<nixpkgs/nixos/modules/profiles/headless.nix>
];
system.stateVersion = "19.03";
};
in {
# containers.consul.autoStart = true; # we've done autostart by default
containers.consul.bindMounts."/var/www" = {
hostPath = "/var/www"; # note how I manage host's path inside container with systemd.tmpfiles
isReadOnly = false;
};
containers.consul.config = { config, pkgs, ...}: {
imports = [
common
];
systemd.tmpfiles.rules = [
"d /var/www/nginx.d 0755 nginx nginx - -"
"d /var/www/nginx.d/logs 0755 nginx nginx - -"
];
services.nginx.enable = true;
services.nginx.appendHttpConfig = ''
include /var/www/nginx.d/*.conf;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log logs/access.log main;
error_log logs/error.log warn;
'';
services.nginx.stateDir = "/var/www/nginx.d";
services.nginx.virtualHosts."_" = {
# fallback virtual host
};
services.nginx.virtualHosts."consul.example.com" = let
consulUiPort = config.services.consul.extraConfig.ports.http or 8500;
in {
listen = [ { addr = "0.0.0.0"; port = 8499; } ];
locations = {
"/" = {
proxyPass = "http://127.0.0.1:${toString consulUiPort}/";
};
};
};
};
# containers.nomad.autoStart = true;
containers.nomad.config = { config, ...}: {
imports = [
common
# ./nomad.nix
];
# services.nomad.enable = true;
};
}
Result
So this will boot two containers, with several custom services, fully managed by NixOS configuration. To control services from host systemctl -M machinename
is used. Or sudo journalctl -M machinename
for logs. For user services defined in home.nix you can use sudo journalctl --user-unit
instead of sudo journalctl -u
. home.nix
allows you to control packages installed on host, envvars, some basic state and dotfiles. And containers bring more NixOS-like experience (if you are used to it).
$ uname -a
Linux nomad-01 4.15.0-1033-aws #35-Ubuntu SMP Wed Feb 6 13:29:46 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
$ nixos-container list
consul
nomad
$ sudo systemctl --user status
Failed to connect to bus: No such file or directory
$ sudo -i systemctl --user status
* nomad-01
State: running
Jobs: 0 queued
Failed: 0 units
Since: Thu 2019-03-07 10:48:49 UTC; 1 weeks 1 days ago
CGroup: /user.slice/user-0.slice/user@0.service
|-nomad-client.service
| |- 904 /nix/store/fx8jzkjz2dbc770vxc3ixbsal4sq67k5-nomad-0.8.7-bin/bin/nomad agent -config /root/nomad-agent.json
| |- 2841 nc -l 0.0.0.0 -p 22908 -q 1
| |- 2846 nc -l 0.0.0.0 -p 26890 -q 1
| |- 2851 nc -l 0.0.0.0 -p 27871 -q 1
| |- 2856 nc -l 0.0.0.0 -p 21980 -q 1
...
But be warned - all this is very hacky and is done only for fun.
Links and tribute:
- @akavel Ubuntu + NixOS-like "overlay"?
- @earvstedt Extra-container - Run declarative containers without full system rebuilds
- @rycee and others for GitHub - nix-community/home-manager: Manage a user environment using Nix [maintainer=@rycee]
- and all, all, all for Nixpkgs/NixOS!