2 month ago I posted a question on reddit about NixOS VMs / Container management in an attempt to use NixOS and it’s various tools to automate the deployment of my homelab. I (naively) thought that i could just replace Terraform / Ansible with NixOS flakes and deploy NixOS LXC container into my proxmox automatically with a CI pipeline whenever i push changes to my nix config, using deploy.rs.
One of the comments on my other post mentionned (microvm.nix)[https://github.com/astro/microvm.nix\], which is a tool to build NixOS VMs and run them on various hypervisor, notably QEMU / KVM. I tried it extensibly for a month now and i’m very happy with the result. It can build very small VMs, with a small footprint which why i used LXC in the first place so it’s a great replacement. So, if my experiments continues to be successful, i now plan to migrate my whole homelab to MicroVMs and write a Flake for each VM i want to deploy.
For the past 2 month i read a lot of documentation and articles about NixOS philosophy, Flakes and it’s usage… All of it was obscure to me and is now much more clear.
I now have :
- A
nixos-minimal-config
flake, which contains a single modulenixosModules.minimalConfig
, which is a minimal configuration containing my user, SSH keys, locales, base packages… It is the equivalent of a cloud-init config. - A
microvm-<SERVICE>
flake, which output is divided in 3 parts :- A call to
minimal-config.nixosModules.minimalConfig
, which install my base config (the input is the git repo on which the flake is pushed) - A configuration part, which enable the desired service and configure it as in a
configuration.nix
file - A call to
microvm.nixosModules.microvm
, which configure my VM (CPU, memory, disk size, network interfaces…)
- A call to
Basically it looks like this :
nixos-minimal-config/flake.nix
:
{
description = "NixOS minimal config flake";
inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; };
outputs = { self, nixpkgs, ... }@inputs: {
nixosModules.minimalConfig = { config, pkgs, modulePath, ... }: {
nix = {
settings.experimental-features = [ "nix-command" "flakes" ];
settings.trusted-users = [ "@wheel" ];
};
networking = {
firewall = {
enable = true;
allowedTCPPorts = [ 22 ];
};
};
time.timeZone = "Europe/Paris";
console.keyMap = "fr";
i18n = { defaultLocale = "fr_FR.UTF-8"; };
security.sudo.wheelNeedsPassword = false;
users = {
users.<USER> = {
isNormalUser = true;
description = "Prénom Nom";
extraGroups = [ "wheel" ];
shell = pkgs.zsh;
openssh.authorizedKeys.keys = [
"sshkey1"
"sshkey-gitea-action-runner"
];
initialPassword = "initialpassword";
};
};
programs = {
zsh = {
enable = true;
shellAliases = {
ll = "ls -l";
lla = "ls -lah";
};
};
tmux = {
enable = true;
};
};
nixpkgs.config.allowUnfree = true;
environment ={
localBinInPath = true;
systemPackages = with pkgs; [
vim
...
];
};
services.openssh = {
enable = true;
settings.PasswordAuthentication = false;
settings.KbdInteractiveAuthentication = false;
settings.PermitRootLogin = "no";
};
system.stateVersion = "24.05";
};
};
}
microvm-gitea/flake.nix
:
{
description = "NixOS in MicroVMs";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
microvm = {
url = "github:astro/microvm.nix";
inputs.nixpkgs.follows = "nixpkgs";
};
minimal-config.url = "git+https://git.repo/nixos_minimal_configuration";
};
outputs = { self, nixpkgs, microvm, minimal-config, ... }:
let
system = "x86_64-linux";
in {
packages.${system} = {
default = self.packages.${system}.my-microvm;
my-microvm = self.nixosConfigurations.my-microvm.config.microvm.declaredRunner;
};
nixosConfigurations = {
my-microvm = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
minimal-config.nixosModules.minimalConfig
({ config, pkgs, ... }:
{
networking.hostName = "gitea";
services.gitea = {
enable = true;
user = "<USER>";
settings = {
server.HTTP_PORT = 3000;
server.ROOT_URL = "http://proxy.address/";
service.DISABLE_REGISTRATION= true;
};
database = {
createDatabase = false;
type = "postgres";
host = "db.address";
user = "gitea";
passwordFile = "/run/secrets/gitea/gitea-dbpass";
};
};
networking.firewall.allowedTCPPorts = [ 3000 ];
})
microvm.nixosModules.microvm
{
microvm = {
volumes = [ {
mountPoint = "/";
image = "/var/lib/microvms/my-microvm/root.img";
size = 8192;
} ];
shares = [
{
proto = "virtiofs";
tag = "ro-store";
source = "/nix/store";
mountPoint = "/nix/.ro-store";
}
{
proto = "virtiofs";
tag = "gitea-env";
source = "/home/tbarnouin/microvm_minimal_config/env/";
mountPoint = "/run/secrets/gitea";
}
];
interfaces = [ {
type = "tap";
id = "vm-test1";
mac = "02:00:00:00:00:01";
} ];
hypervisor = "qemu";
socket = "control.socket";
};
systemd.network.enable = true;
systemd.network.networks."20-lan" = {
matchConfig.Type = "ether";
networkConfig = {
Address = [""];
Gateway = "";
DNS = [""];
IPv6AcceptRA = true;
DHCP = "no";
};
};
}
];
};
};
};
}
I wonder if it is the “right way” to use flakes ? Maybe my base config should be a single configuration file i can copy on every new flakes i write ? Or maybe every flake i write for a service should be an independent module, and i should juste call those 3 module onto a fourth flake gluing everything together ? Doing so, i think i reproduce some ansible mechanism i’m used to, and maybe it is not the NixOS way of doing things.
Also, i would like to template those flake so i can input variables on my services’ flake and pass them to the minimal_config
one. That way i can pass a different username, add an SSH key or anything else. I don’t know any way to do this ? I also don’t if this is (again) a automatism i have from ansible and if there is another NixOS way to achieve it.