feat: initial version of the full setup

This commit is contained in:
Xavier Morel
2025-10-23 19:36:05 +02:00
commit cc957061de
12 changed files with 841 additions and 0 deletions

5
.envrc Normal file
View File

@@ -0,0 +1,5 @@
export DIRENV_WARN_TIMEOUT=20s
eval "$(devenv direnvrc)"
use flake --no-pure-eval

19
.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
# Direnv & Devenv
.direnv
.envrc.local
.devenv
.devenv.flake.nix
# should be Terranix .nix files instead
*.tf
*.tfvars
*.tfstate*
.terraform*
# Outputs
config.tf.json
nixos-template
# Possibly sensitive for public repo
infra/ips.nix
infra/constants.nix

51
README.md Normal file
View File

@@ -0,0 +1,51 @@
# NixOS x Proxmox infra configuration with Terranix
This repository allows to manage LXC containers on Proxmox.
It's supposed to be used from an host where `nixos-rebuild` is available.
=> Uses `nix-community/generators` to build a LXC template with a base NixOS container
=> Uses `terranix` to build the infra definition and `opentofu` to deploy it on Proxmox
=> Uses `nixos-rebuild` to deploy the configuration on the container
My main objective was to have a "light" definition for the containers and to be able to use Nix to factorize configuration.
# Usage
## Prepare the infra constants
-> `cp infra/constants.nix.template infra/constants.nix`
-> adapt `infra/constants.nix` to match your needs
-> touch `infra/ips.nix`
-> remove both these files from `.gitignore` and `git add` them.
## Build NixOS template
-> modify `infra/lxc-template.nix` as needed
-> run `build-template`
-> template available in `nixos-template/tarball/`
(.tar.xz to be uploaded to Proxmox)
TODO Script the Proxmox Template upload if possible.
## Prepare Terraform
-> create a user/role etc on Proxmox (see [the provider documentation](https://registry.terraform.io/providers/Telmate/proxmox/latest/docs))
-> `cp terraform.tfvars.example terraform.tfvars`
-> edit `terraform.tfvars` to fill in values
-> adapt the terraform base config as needed in `infra/main.nix`
-> run `tofu init`
## Adapt NixOS / Terraform modules building
-> edit `lib/containers.nix` to change how a container definition is translated to TF / NixOS config (in particular check the template name)
## Create containers definitions
-> `cp containers/lxc-cont.nix.template containers/lxc-#NAME#.nix`
-> edit `containers/lxc-#NAME#.nix` as needed
-> run `build-terraform-json`
-> run `tofu plan` and review the plan
-> run `tofu apply`, hopefully without errors
-> run `deploy #NAME#`
## Update container
-> edit `containers/lxc-#NAME#.nix` as needed
-> if the container specs have changed, do all as above
-> otherwise you can just run `deploy #NAME#`

381
flake.lock generated Normal file
View File

@@ -0,0 +1,381 @@
{
"nodes": {
"cachix": {
"inputs": {
"devenv": [
"devenv"
],
"flake-compat": [
"devenv"
],
"git-hooks": [
"devenv",
"git-hooks"
],
"nixpkgs": [
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1752264895,
"narHash": "sha256-1zBPE/PNAkPNUsOWFET4J0cjlvziH8DOekesDmjND+w=",
"owner": "cachix",
"repo": "cachix",
"rev": "47053aef762f452e816e44eb9a23fbc3827b241a",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "latest",
"repo": "cachix",
"type": "github"
}
},
"devenv": {
"inputs": {
"cachix": "cachix",
"flake-compat": "flake-compat",
"flake-parts": "flake-parts",
"git-hooks": "git-hooks",
"nix": "nix",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1761091275,
"narHash": "sha256-SIiugXvSuI2WFedt1NyDj8yHsSDntsO/JWKyEZ+mI50=",
"owner": "cachix",
"repo": "devenv",
"rev": "a795c32dc826b51d12706f27fb344f966bb2b084",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "devenv",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1747046372,
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1756770412,
"narHash": "sha256-+uWLQZccFHwqpGqr2Yt5VsW/PbeJVTn9Dk6SHWhNRPw=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "4524271976b625a4a605beefd893f270620fd751",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_2": {
"inputs": {
"nixpkgs-lib": [
"terranix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1736143030,
"narHash": "sha256-+hu54pAoLDEZT9pjHlqL9DNzWz0NbUn8NEAHP7PQPzU=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "b905f6fc23a9051a6e1b741e1438dbfc0634c6de",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"generators": {
"inputs": {
"nixlib": "nixlib",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1751903740,
"narHash": "sha256-PeSkNMvkpEvts+9DjFiop1iT2JuBpyknmBUs0Un0a4I=",
"owner": "nix-community",
"repo": "nixos-generators",
"rev": "032decf9db65efed428afd2fa39d80f7089085eb",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixos-generators",
"type": "github"
}
},
"git-hooks": {
"inputs": {
"flake-compat": [
"devenv",
"flake-compat"
],
"gitignore": "gitignore",
"nixpkgs": [
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1758108966,
"narHash": "sha256-ytw7ROXaWZ7OfwHrQ9xvjpUWeGVm86pwnEd1QhzawIo=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "54df955a695a84cd47d4a43e08e1feaf90b1fd9b",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"devenv",
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nix": {
"inputs": {
"flake-compat": [
"devenv",
"flake-compat"
],
"flake-parts": [
"devenv",
"flake-parts"
],
"git-hooks-nix": [
"devenv",
"git-hooks"
],
"nixpkgs": [
"devenv",
"nixpkgs"
],
"nixpkgs-23-11": [
"devenv"
],
"nixpkgs-regression": [
"devenv"
]
},
"locked": {
"lastModified": 1758763079,
"narHash": "sha256-Bx1A+lShhOWwMuy3uDzZQvYiBKBFcKwy6G6NEohhv6A=",
"owner": "cachix",
"repo": "nix",
"rev": "6f0140527c2b0346df4afad7497baa08decb929f",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "devenv-2.30.5",
"repo": "nix",
"type": "github"
}
},
"nixlib": {
"locked": {
"lastModified": 1736643958,
"narHash": "sha256-tmpqTSWVRJVhpvfSN9KXBvKEXplrwKnSZNAoNPf/S/s=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "1418bc28a52126761c02dd3d89b2d8ca0f521181",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1758532697,
"narHash": "sha256-bhop0bR3u7DCw9/PtLCwr7GwEWDlBSxHp+eVQhCW9t4=",
"owner": "cachix",
"repo": "devenv-nixpkgs",
"rev": "207a4cb0e1253c7658c6736becc6eb9cace1f25f",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "rolling",
"repo": "devenv-nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1736657626,
"narHash": "sha256-FWlPMUzp0lkQBdhKlPqtQdqmp+/C+1MBiEytaYfrCTY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2f9e2f85cb14a46410a1399aa9ea7ecf433e422e",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1760878510,
"narHash": "sha256-K5Osef2qexezUfs0alLvZ7nQFTGS9DL2oTVsIXsqLgs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5e2a59a5b1a82f89f2c7e598302a9cacebb72a67",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1728956102,
"narHash": "sha256-J8zo+UYNjHATsxn2/ROl8iaji2RgLm+sG7b3VcD36YM=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "3d85bae2431f20ab1ac5cf14d03d314dffe629af",
"type": "github"
},
"original": {
"owner": "nixos",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"flake-utils": "flake-utils",
"generators": "generators",
"nixpkgs": "nixpkgs_3",
"terranix": "terranix"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"terranix": {
"inputs": {
"flake-parts": "flake-parts_2",
"nixpkgs": "nixpkgs_4",
"systems": "systems_2"
},
"locked": {
"lastModified": 1757278723,
"narHash": "sha256-hTMi6oGU+6VRnW9SZZ+muFcbfMEf2ajjOp7Z2KM5MMY=",
"owner": "terranix",
"repo": "terranix",
"rev": "924573fa6587ac57b0d15037fbd2d3f0fcdf17fb",
"type": "github"
},
"original": {
"owner": "terranix",
"repo": "terranix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

138
flake.nix Normal file
View File

@@ -0,0 +1,138 @@
{
description = "Infrastructure LXC + Terraform + NixOS via Flakes";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
generators.url = "github:nix-community/nixos-generators";
terranix.url = "github:terranix/terranix";
devenv.url = "github:cachix/devenv";
};
nixConfig = {
extra-trusted-public-keys = "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=";
extra-substituters = "https://devenv.cachix.org";
};
outputs =
{
self,
nixpkgs,
flake-utils,
generators,
terranix,
devenv,
...
}@inputs:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
lib = pkgs.lib;
containersMapping = import ./infra/ips.nix;
containers = import ./lxc { inherit pkgs containersMapping; };
lxc-def = import ./infra/lxc-template.nix;
infra = import ./infra/constants.nix;
nixosConfigurations = lib.mapAttrs (
_: def:
nixpkgs.lib.nixosSystem {
inherit system;
modules = [ def.nixosModule ];
}
) containers;
terraformCfg = import ./infra;
terraformResources = {
resource.proxmox_lxc = lib.mapAttrs (_: def: def.terraformResource) containers;
};
in
{
packages.${system} = {
lxc-template = generators.nixosGenerate {
system = "x86_64-linux";
modules = [ lxc-def ];
format = "proxmox-lxc";
};
terraform-json = terranix.lib.terranixConfiguration {
inherit system;
modules = [
terraformResources
terraformCfg
];
};
};
nixosConfigurations = nixosConfigurations;
devShells.${system}.default = devenv.lib.mkShell {
inherit inputs pkgs;
modules = [
(
{ pkgs, config, ... }:
{
languages.opentofu.enable = true;
scripts.build-template.exec = ''
nix build .#lxc-template -o nixos-template
echo 'Template should be available at nixos-template/tarball/*.tar.xz'
'';
scripts.build-terraform-json.exec = ''
nix build .#terraform-json -o config.tf.json
echo 'Terraform build available as config.tf.json'
'';
scripts.add-lxc.exec = ''
if ! [[ "$2" =~ ^[0-9]+$ ]]; then
echo "Error: invalid container ID '$2', should be a number" && exit
fi
if ! [ -f infra/ips.nix ]; then
echo "{" > infra/ips.nix
echo "}" >> infra/ips.nix
fi
if ! [[ -z "`grep "[^0-9]$2[^0-9]" infra/ips.nix`" ]]; then
echo "Error: container ID '$2' already used" && exit
fi
if [ -f lxc/$1.nix ]; then
echo "Error: container definition '$1' already exists" && exit
fi
sed -i "s#}# $1 = $2;#" infra/ips.nix
echo "}" >> infra/ips.nix
cp lxc/container.nix.template lxc/$1.nix
git add lxc/$1.nix
echo "Entry added to infra/ips.nix"
echo "Container template copied to lxc/$1.nix, please edit it"
'';
scripts.deploy-lxc.exec = ''
if [ -f lxc/$1.nix ]; then
CONTID=`grep -E "$1 ?=" infra/ips.nix | cut -d '=' -f 2 | grep -o '\<[0-9]*\>' `
echo "Redeploying LXC on container '$1' ('$CONTID')"
nixos-rebuild switch --flake .#$1 --target-host root@${infra.ip_prefix}$CONTID
echo "Done."
else
echo "Error: Container definition 'lxc/$1.nix' not found!"
fi
'';
enterShell = ''
echo "Helper commands available:"
echo ""
echo "'build-template' to build the Proxmox LXC NixOS template"
echo "'build-terraform-json' to build the Terraform config.tf.json file to apply"
echo "'add-lxc' to prepare the template for a LXC container"
echo "'deploy-lxc' to deploy a container configuration using nixos-rebuild"
'';
}
)
];
};
};
}

View File

@@ -0,0 +1,19 @@
{
# Centralizes the IP to the gateway for the containers.
gateway_ip = "10.0.0.1";
# Builders for IP addresses, given a container id.
ip_prefix = "10.0.0.";
cidr = "24";
build_ip = id: "${ip_prefix}${toString id}";
build_ip_cidr = id: "${ip_prefix}${toString id}/${cidr}";
# Your deployer's host
master_public_ssh_key = "ssh-ed25519 [...] me@here";
# Default timezone for the containers
default_tz = "UTC";
# NixOS template build name => see `ls nixos-template/tarball/`
nixos_template_name = "nixos-image-lxc-proxmox-25.11pre-git-x86_64-linux";
}

21
infra/default.nix Normal file
View File

@@ -0,0 +1,21 @@
{ lib, ... }:
{
terraform.required_providers = {
proxmox = {
source = "Telmate/proxmox";
version = "~> 2.9.11";
};
};
provider.proxmox = {
pm_api_url = "\${var.pm_api_url}";
pm_api_token_id = "\${var.pm_api_token_id}";
pm_api_token_secret = "\${var.pm_api_token_secret}";
pm_tls_insecure = false;
};
variable.pm_api_url.type = "string";
variable.pm_api_token_id.type = "string";
variable.pm_api_token_secret.type = "string";
variable.pve_node.type = "string";
}

57
infra/lxc-template.nix Normal file
View File

@@ -0,0 +1,57 @@
{
pkgs,
lib,
modulesPath,
...
}:
let
infra = import ./constants.nix;
in
{
imports = [
(modulesPath + "/virtualisation/proxmox-lxc.nix")
];
boot.isContainer = true;
systemd.suppressedSystemUnits = [
"dev-mqueue.mount"
"sys-kernel-debug.mount"
"sys-fs-fuse-connections.mount"
];
environment.systemPackages = with pkgs; [
vim
openssl
coreutils
];
services.openssh.enable = true;
services.chrony = {
enable = true;
enableNTS = true;
servers = [ "time.cloudflare.com" ];
};
nix.settings = {
experimental-features = [
"nix-command"
"flakes"
];
auto-optimise-store = true;
};
nix.gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 7d";
};
time.timeZone = infra.default_tz;
users.users.root = {
openssh.authorizedKeys.keys = [
infra.master_public_ssh_key
];
};
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
system.stateVersion = "25.11";
}

69
lib/containers.nix Normal file
View File

@@ -0,0 +1,69 @@
{ def, ... }:
let
infra = import ../infra/constants.nix;
hostname = def.hostname;
memory = def.memory or 512;
cores = def.cores or 1;
container_id = def.container_id;
disk = def.disk or "4G";
swap = def.swap or null; # TODO: Implement
services = def.services or { };
open_ports = def.open_ports or [ ];
other_packages = def.other_packages or [ ];
etc = def.etc or { };
logging_enabled = def.logging.enable or false; # TODO: Implement
logging_metrics_enabled = def.logging.metrics.enable or false;
extraModules = def.extraModules or [ ];
template = def.template or infra.nixos_template_name;
unprivileged = def.unprivileged or true;
tags = def.tags or "";
in
{
terraformResource = {
hostname = hostname;
memory = memory;
cores = cores;
ostemplate = "local:vztmpl/${template}.tar.xz";
unprivileged = unprivileged;
password = "changeme";
features.nesting = true;
target_node = "\${var.pve_node}";
network = {
name = "eth0";
bridge = "vmbr0";
ip = infra.build_ip_cidr container_id;
gw = infra.gateway_ip;
type = "veth";
};
rootfs = {
storage = "local-lvm";
size = disk;
};
vmid = container_id;
tags = "terraform;${tags}";
};
nixosModule =
{ config, pkgs, ... }:
{
imports = [
../infra/lxc-template.nix
]
++ extraModules;
networking.hostName = hostname;
networking.firewall.allowedTCPPorts = open_ports;
services = services;
environment.etc = etc;
environment.systemPackages = other_packages;
# logging things...
# # logs configuration ...
# # environment.etc."alloy/config.alloy" = '' loki blabla '';
# # environment.etc."alloy/metrics.alloy" = '' prometheus blabla '';
# #
# # -> services.alloy.extraFlags = [
# # "--server.http.listen-addr=127.0.0.1:12346"
# # "--disable-reporting"
# # ]
};
}

View File

@@ -0,0 +1,41 @@
{ pkgs, containersMapping, ... }:
let
infra = import ../infra/constants.nix;
in
{
# OPTIONAL int cores: number of CPU (default = 1)
cores = 2;
# OPTIONAL int memory: RAM memory (default 512)
memory = 512;
# OPTIONAL string disk: disk space (default "4G") - beware, NixOS is greedy
disk = "4G";
# OPTIONAL string swap: swap space (default null)
swap = null;
# OPTIONAL list of int ports: ports to open (TCP tho) (default [])
ports = [ 80 ];
# OPTIONAL submodule services: services to be passed to the NixOS Module (default {})
services = {
nginx.enable = true;
};
# OPTIONAL list of pkgs other_packages: packages to add to eenvironment.systemPackages (default [])
other_packages = [ pkgs.hello ];
# OPTIONAL submodule etc: files contents to pass to eenvironment.etc
etc."alloy/log-myservice.alloy" = ''
# logger_ip = ${infra.build_ip containersMapping.grafana}
# prometheus = ${infra.build_ip containersMapping.prometheus}
'';
# OPTIONAL bool logging.enable: whether to enable the Alloy configuration (=> Loki)
# Need further configuration in etc."alloy/log-myservice.alloy"
logging.enable = true;
# OPTIONAL bool logging.metrics.enable: whether to enable the Alloy metrics configuration (=> Prometheus)
logging.metrics.enable = true;
}

36
lxc/default.nix Normal file
View File

@@ -0,0 +1,36 @@
{ pkgs, containersMapping, ... }:
let
lib = pkgs.lib;
containerBuild = import ../lib/containers.nix;
containersFiles = builtins.readDir ./.;
containers = lib.filterAttrs (_: v: v != null) (
lib.mapAttrs (
name: type:
if type == "regular" && name != "default.nix" && lib.hasSuffix ".nix" name then
import ./${name} { inherit containersMapping pkgs; }
else
null
) containersFiles
);
cleanedName = lib.listToAttrs (lib.mapAttrsToList (name: def: mkContainer name def) containers);
mkContainer =
name: raw_def:
let
hostname = lib.removeSuffix ".nix" name;
def = raw_def // {
hostname = hostname;
container_id = containersMapping.${hostname};
};
result = containerBuild { inherit def; };
in
{
name = hostname;
value = result;
};
in
cleanedName

4
terraform.tfvars.example Normal file
View File

@@ -0,0 +1,4 @@
pm_api_url = "https://proxmox:8006/api2/json"
pm_api_token_id = "terraform@pve"
pm_api_token_secret = "secr3t"
pve_node = "proxmox"