home lab init

This commit is contained in:
plasmagoat 2025-06-03 23:07:46 +02:00
commit 7278922625
65 changed files with 27336 additions and 0 deletions

1
nixos/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
result

5
nixos/README.md Normal file
View file

@ -0,0 +1,5 @@
nixos-rebuild switch --flake .#traefik --target-host root@192.168.1.171 --verbose
nixos-rebuild switch --flake .#proxmox --target-host root@192.168.1.205 --verbose
nixos-rebuild switch --flake .#sandbox --target-host root@192.168.1.148 --verbose
nixos-rebuild switch --flake .#monitoring --target-host root@192.168.1.88 --verbose
nixos-rebuild switch --flake .#forgejo --target-host root@192.168.1.249 --verbose

112
nixos/configuration.nix Normal file
View file

@ -0,0 +1,112 @@
{ config, pkgs, modulesPath, lib, ... }:
{
########################################################################
# IMPORTS & PROFILE
#
# We rely on the QEMU Guest Agent profile so that Proxmox can talk
# to the VMs guest-agent. Both “live” and “template” need this.
########################################################################
imports = [
# Enables QEMU Guest Agent support in the VM
(modulesPath + "/profiles/qemu-guest.nix")
];
config = {
########################################################################
# A) COMMON SETTINGS
########################################################################
# Provide a default hostname
networking.hostName = lib.mkDefault "base";
# Nixpkgs & Unfree
# Allow unfree packages if you ever need them.
nixpkgs.config.allowUnfree = true;
# QEMU Guest Agent (Proxmox integration)
# Ensure the qemu-guest-agent service is enabled so Proxmox can query
# the VM for IPs, etc.
services.qemuGuest.enable = lib.mkDefault true;
# GRUB on the “boot drive”
# Both live and template should install a bootloader on /dev/disk/by-label/nixos.
boot.loader.grub.enable = lib.mkDefault true;
boot.loader.grub.devices = [ "nodev" ];
# Grow the root partition on first boot
boot.growPartition = lib.mkDefault true;
# Sudo: Do not require a password for wheel group
security.sudo.wheelNeedsPassword = false;
# OpenSSH: disable passwordbased auth, only allow keybased
services.openssh = {
enable = true;
settings.PermitRootLogin = "prohibit-password";
settings.PasswordAuthentication = false;
settings.KbdInteractiveAuthentication = false;
};
programs.ssh.startAgent = true;
# Roots SSH authorized_keys (copy your own keys here)
# Both live & template will install these, so you can ssh in.
users.users.root.openssh.authorizedKeys.keys = [
# ← Replace these with your actual public keys
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCeg/n/vst9KME8byhxX2FhA+FZNQ60W38kkNt45eNzK5zFqBYuwo1nDXVanJSh9unRvB13b+ygpZhrb4sHvkETGWiEioc49MiWr8czEhu6Wpo0vv5MAJkiYvGZUYPdUW52jUzWcYdw8PukG2rowrxL5G0CmsqLwHMPU2FyeCe5aByFI/JZb8R80LoEacgjUiipJcoLWUVgG2koMomHClqGu+16kB8nL5Ja3Kc9lgLfDK7L0A5R8JXhCjrlEsmXbxZmwDKuxvjDAZdE9Sl1VZmMDfWkyrRlenrt01eR3t3Fec6ziRm5ZJk9e2Iu1DPoz+PoHH9aZGVwmlvvnr/gMF3OILxcqb0qx+AYlCCnb6D6pJ9zufhZkKcPRS1Q187F6fz+v2oD1xLZWFHJ92+7ItM0WmbDOHOC29s5EA6wNm3iXZCq86OI3n6T34njDtPqh6Z7Pk2sdK4GBwnFj4KwEWXvdKZKSX1qb2EVlEBE9QI4Gf3eg4SiBu2cAFt3nOSzs8c= asol\\dbs@ALPHA-DBS-P14sG2"
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+U3DWOrklcA8n8wdbLBGyli5LsJI3dpL2Zod8mx8eOdC4H127ZT1hzuk2uSmkic4c73BykPyQv8rcqwaRGW94xdMRanKmHYxnbHXo5FBiGrCkNlNNZuahthAGO49c6sUhJMq0eLhYOoFWjtf15sr5Zu7Ug2YTUL3HXB1o9PZ3c9sqYHo2rC/Il1x2j3jNAMKST/qUZYySvdfNJEeQhMbQcdoKJsShcE3oGRL6DFBoV/mjJAJ+wuDhGLDnqi79nQjYfbYja1xKcrKX+D3MfkFxFl6ZIzomR1t75AnZ+09oaWcv1J7ehZ3h9PpDBFNXvzyLwDBMNS+UYcH6SyFjkUbF David@NZXT"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICUP7m8jZJiclZGfSje8CeBYFhX10SrdtjYziuChmj1X plasmagoat@macbook-air"
];
# Default filesystem on
fileSystems."/" = lib.mkDefault {
device = "/dev/disk/by-label/nixos";
autoResize = true; # grow on first boot
fsType = "ext4";
};
# Timezone & Keyboard
time.timeZone = "Europe/Copenhagen";
console.keyMap = "dk-latin1";
# Default set of packages
environment.systemPackages = with pkgs; [
vim # emergencies
git # pulling flakes, code
curl # downloading things
python3 # for Ansible if needed on live VM
];
# Nix settings (cache, experimental, gc)
nix.settings.trusted-users = [ "root" "@wheel" ];
nix.settings.experimental-features = [ "nix-command" "flakes" ];
nix.extraOptions = ''
experimental-features = nix-command flakes
keep-outputs = true
keep-derivations = true
'';
nix.gc.automatic = true;
nix.gc.dates = "weekly";
nix.gc.options = "--delete-older-than 7d";
# mDNS with avahi to enable .local dns
services.avahi = {
enable = true;
openFirewall = true;
publish = {
enable = true;
addresses = true;
domain = true;
};
nssmdns4 = true;
nssmdns6 = false;
ipv6 = false;
};
networking.firewall.allowedUDPPorts = [ 5353 ];
# State version (set to match the Nixpkgs youre using)
system.stateVersion = lib.mkDefault "25.05";
};
}

84
nixos/flake.lock generated Normal file
View file

@ -0,0 +1,84 @@
{
"nodes": {
"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"
}
},
"nixos-generators": {
"inputs": {
"nixlib": "nixlib",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1747663185,
"narHash": "sha256-Obh50J+O9jhUM/FgXtI3he/QRNiV9+J53+l+RlKSaAk=",
"owner": "nix-community",
"repo": "nixos-generators",
"rev": "ee07ba0d36c38e9915c55d2ac5a8fb0f05f2afcc",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixos-generators",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1748809735,
"narHash": "sha256-UR5vKj8rwKQmE8wxKFHgoJKbod05DMoH5phTje4L1l8=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "2dd418dd6def2485b552bfdeefec9cbed2b3e583",
"type": "github"
},
"original": {
"owner": "nixos",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixos-generators": "nixos-generators",
"nixpkgs": "nixpkgs",
"sops-nix": "sops-nix"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1747603214,
"narHash": "sha256-lAblXm0VwifYCJ/ILPXJwlz0qNY07DDYdLD+9H+Wc8o=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "8d215e1c981be3aa37e47aeabd4e61bb069548fd",
"type": "github"
},
"original": {
"owner": "Mic92",
"repo": "sops-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

96
nixos/flake.nix Normal file
View file

@ -0,0 +1,96 @@
{
description = "Unified flake for Proxmox base image + live NixOS VMs";
inputs = {
# Nixpkgs repo for system packages
nixpkgs.url = "github:nixos/nixpkgs";
# nixos-generators lets us produce a "proxmox"-formatted image
nixos-generators = {
url = "github:nix-community/nixos-generators";
inputs.nixpkgs.follows = "nixpkgs";
};
# sops-nix secret management
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, nixos-generators, sops-nix,... }:
let
system = "x86_64-linux";
################################################################################
# A) Define “live” NixOS VM configurations under nixosConfigurations
################################################################################
liveVMs = {
traefik = nixpkgs.lib.nixosSystem {
inherit system;
modules = [ ./hosts/traefik/host.nix ];
};
sandbox = nixpkgs.lib.nixosSystem {
inherit system;
modules = [ ./hosts/sandbox/host.nix ];
};
monitoring = nixpkgs.lib.nixosSystem {
inherit system;
modules = [ ./hosts/monitoring/host.nix ];
};
forgejo = nixpkgs.lib.nixosSystem {
inherit system;
modules = [ ./hosts/forgejo/host.nix sops-nix.nixosModules.sops ];
};
# dockerHost = pkgs.lib.nixosSystem {
# inherit system;
# modules = [
# ./configuration.nix
# ./users/plasmagoat.nix
# ./hosts/docker-host.nix # DockerHost VM settings (shown below)
# ];
# };
};
################################################################################
# B) Use nixos-generators to produce “template” images for Proxmox
################################################################################
# 1) Existing Proxmox “base” image generator
base = nixos-generators.nixosGenerate {
system = "x86_64-linux";
modules = [ ./templates/base.nix ];
format = "proxmox"; # outputs a .vma.zst suitable for qmrestore
};
# 2) A “docker” generator which builds a Proxmoxready template
docker = nixos-generators.nixosGenerate {
system = "x86_64-linux";
modules = [ ./templates/docker.nix ];
format = "proxmox";
};
in
{
################################################################################
# 1) Export “live” VM configs so you can run:
# nixos-rebuild switch --flake .#traefik --target-host root@<traefik-IP>
# nixos-rebuild switch --flake .#sandbox --target-host root@<sandbox-IP>
# nixos-rebuild switch --flake .#dockerHost --target-host root@<dockerHost-IP>
################################################################################
nixosConfigurations = liveVMs;
################################################################################
# 2) Export Proxmox template images under packages.x86_64-linux:
#
# • proxmox → `nix build .#proxmox` (generic base)
# • docker → `nix build .#docker` (docker template)
################################################################################
packages.x86_64-linux = {
base = base;
docker = docker;
};
};
}

View file

@ -0,0 +1,46 @@
{ config, pkgs, modulesPath, lib, ... }:
{
# Pull in all the shared settings from configuration.nix
imports = [ ../configuration.nix ];
config = lib.recursiveUpdate config ({
# (Here, add anything liveVMspecific—e.g. NFS mounts, Docker, Compose service,
# static IP, or “import users/plasmagoat.nix” if you prefer.)
networking.interfaces.enp0s25 = {
useDHCP = false;
ipv4.addresses = [ { address = "192.168.1.50"; prefixLength = 24; } ];
ipv4.gateway = "192.168.1.1";
};
# Docker + Compose bits, for example:
fileSystems."/mnt/nas" = {
device = "192.168.1.100:/export/docker-volumes";
fsType = "nfs";
options = [ "defaults" "nofail" "x-systemd.requires=network-online.target" ];
};
environment.systemPackages = with pkgs; [
pkgs.docker
pkgs.docker-compose
# …plus anything else you want only on live VM…
];
services.docker.enable = true;
systemd.services.dockerComposeApp = {
description = "Auto-start DockerCompose stack";
after = [ "network-online.target" "docker.service" ];
wants = [ "network-online.target" "docker.service" ];
serviceConfig = {
WorkingDirectory = "/etc/docker-compose-app";
ExecStart = "${pkgs.docker-compose}/bin/docker-compose -f /etc/docker-compose-app/docker-compose.yml up";
ExecStop = "${pkgs.docker-compose}/bin/docker-compose -f /etc/docker-compose-app/docker-compose.yml down";
Restart = "always";
RestartSec = 10;
};
wantedBy = [ "multi-user.target" ];
};
});
}

View file

@ -0,0 +1,17 @@
🥇 Phase 1: Git + Secrets
✅ Set up Forgejo VM (NixOS declarative)
✅ Set up sops-nix + age keys (can live in the Git repo)
✅ Push flake + ansible + secrets to Forgejo
✅ Write a basic README with how to rebuild infra
🥈 Phase 2: GitOps
🔁 Add CI runner VM
🔁 Configure runner to deploy (nixos-rebuild or ansible-playbook) on commit
🔁 Optional: add webhooks to auto-trigger via Forgejo

View file

@ -0,0 +1,31 @@
{ lib, pkgs, config, ... }:
{
systemd.services.forgejo = {
after = [ "postgresql.service" ];
requires = [ "postgresql.service" ];
};
services.postgresql = {
enable = true;
ensureDatabases = [ "forgejo" ];
ensureUsers = [
{
name = "forgejo";
ensureDBOwnership = true;
}
];
authentication = pkgs.lib.mkOverride 10 ''
#type database DBuser auth-method
local all all trust
'';
};
services.prometheus.exporters.postgres = {
enable = true;
listenAddress = "0.0.0.0";
port = 9187;
};
networking.firewall.allowedTCPPorts = [ 9187 ];
}

View file

@ -0,0 +1,65 @@
{ lib, pkgs, config, ... }:
let
cfg = config.services.forgejo;
srv = cfg.settings.server;
domain = "git.procopius.dk";
in
{
users.users.plasmagoat.extraGroups = [ "forgejo" ];
services.forgejo = {
enable = true;
user = "forgejo";
group = "forgejo";
stateDir = "/srv/forgejo";
settings = {
# https://forgejo.org/docs/latest/admin/config-cheat-sheet/
server = {
DOMAIN = domain;
ROOT_URL = "https://${srv.DOMAIN}/";
PROTOCOL = "http";
HTTP_PORT = 3000;
};
database = {
DB_TYPE = lib.mkForce "postgres";
HOST = "/run/postgresql";
NAME = "forgejo";
USER = "forgejo";
};
service = {
DISABLE_REGISTRATION = true;
};
metrics = {
ENABLED = true;
ENABLED_ISSUE_BY_REPOSITORY = true;
ENABLED_ISSUE_BY_LABEL = true;
};
# log = {
# ROOT_PATH = "/var/log/forgejo";
# MODE = "file";
# LEVEL = "Info";
# };
security = {
INSTALL_LOCK = true;
SECRET_KEY = "changeme"; # can be another secret
};
};
};
sops.secrets.forgejo-admin-password.owner = "forgejo";
sops.secrets.forgejo-db-password.owner = "forgejo";
systemd.services.forgejo.preStart = let
adminCmd = "${lib.getExe cfg.package} admin user";
user = "plasmagoat"; # Note, Forgejo doesn't allow creation of an account named "admin"
pwd = config.sops.secrets.forgejo-admin-password;
in ''
${adminCmd} create --admin --email "root@localhost" --username ${user} --password "$(tr -d '\n' < ${pwd.path})" || true
## uncomment this line to change an admin user which was already created
# ${adminCmd} change-password --username ${user} --password "$(tr -d '\n' < ${pwd.path})" || true
'';
# Optional: firewall
networking.firewall.allowedTCPPorts = [ 3000 ];
}

View file

@ -0,0 +1,12 @@
{ config, pkgs, modulesPath, lib, ... }:
{
imports = [
../../templates/base.nix
../../secrets/sops.nix
./networking.nix
./storage.nix
./forgejo.nix
./database.nix
];
}

View file

@ -0,0 +1,6 @@
{ config, lib, pkgs, ... }: {
networking = {
hostName = "forgejo";
};
}

View file

@ -0,0 +1,29 @@
{
# services.nfs.client = {
# enable = true;
# idmapd.enable = true;
# };
# environment.etc."idmapd.conf".text = ''
# [General]
# Domain = localdomain
# [Mapping]
# Nobody-User = nobody
# Nobody-Group = nogroup
# '';
boot.supportedFilesystems = [ "nfs" ];
services.rpcbind.enable = true;
fileSystems."/srv/forgejo" = {
device = "192.168.1.226:/volume1/data/forgejo";
fsType = "nfs4";
options = [ "x-systemd.automount" "noatime" "_netdev" ];
};
systemd.tmpfiles.rules = [
"d /srv/forgejo 0750 forgejo forgejo -"
];
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,744 @@
{
"__inputs": [],
"__requires": [
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "7.5.5"
},
{
"type": "panel",
"id": "graph",
"name": "Graph",
"version": ""
},
{
"type": "panel",
"id": "piechart",
"name": "Pie chart v2",
"version": ""
},
{
"type": "datasource",
"id": "prometheus",
"name": "Prometheus",
"version": "1.0.0"
},
{
"type": "panel",
"id": "singlestat",
"name": "Singlestat",
"version": ""
}
],
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"description": "Traefik dashboard prometheus",
"editable": true,
"gnetId": 4475,
"graphTooltip": 0,
"id": null,
"iteration": 1620932097756,
"links": [],
"panels": [
{
"datasource": null,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 10,
"title": "$backend stats",
"type": "row"
},
{
"cacheTimeout": null,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"decimals": 0,
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 0,
"y": 1
},
"id": 2,
"interval": null,
"links": [],
"maxDataPoints": 3,
"options": {
"displayLabels": [],
"legend": {
"calcs": [],
"displayMode": "table",
"placement": "right",
"values": ["value", "percent"]
},
"pieType": "pie",
"reduceOptions": {
"calcs": ["lastNotNull"],
"fields": "",
"values": false
},
"text": {}
},
"targets": [
{
"exemplar": true,
"expr": "traefik_service_requests_total{service=\"$service\"}",
"format": "time_series",
"interval": "",
"intervalFactor": 2,
"legendFormat": "{{method}} : {{code}}",
"refId": "A"
}
],
"title": "$service return code",
"type": "piechart"
},
{
"cacheTimeout": null,
"colorBackground": false,
"colorValue": false,
"colors": ["#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a"],
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {},
"overrides": []
},
"format": "ms",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 7,
"w": 12,
"x": 12,
"y": 1
},
"id": 4,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": true
},
"tableColumn": "",
"targets": [
{
"exemplar": true,
"expr": "sum(traefik_service_request_duration_seconds_sum{service=\"$service\"}) / sum(traefik_service_requests_total{service=\"$service\"}) * 1000",
"format": "time_series",
"interval": "",
"intervalFactor": 2,
"legendFormat": "",
"refId": "A"
}
],
"thresholds": "",
"title": "$service response time",
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "avg"
},
{
"aliasColors": {},
"bars": true,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 7,
"w": 24,
"x": 0,
"y": 8
},
"hiddenSeries": false,
"id": 3,
"legend": {
"alignAsTable": true,
"avg": true,
"current": false,
"max": true,
"min": true,
"rightSide": true,
"show": true,
"total": false,
"values": true
},
"lines": false,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "7.5.5",
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"exemplar": true,
"expr": "sum(rate(traefik_service_requests_total{service=\"$service\"}[5m]))",
"format": "time_series",
"interval": "",
"intervalFactor": 2,
"legendFormat": "Total requests $service",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Total requests over 5min $service",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"collapsed": false,
"datasource": null,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 15
},
"id": 12,
"panels": [],
"title": "Global stats",
"type": "row"
},
{
"aliasColors": {},
"bars": true,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 7,
"w": 12,
"x": 0,
"y": 16
},
"hiddenSeries": false,
"id": 5,
"legend": {
"alignAsTable": true,
"avg": false,
"current": true,
"max": true,
"min": true,
"rightSide": true,
"show": true,
"total": false,
"values": true
},
"lines": false,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "7.5.5",
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": true,
"steppedLine": false,
"targets": [
{
"expr": "rate(traefik_entrypoint_requests_total{entrypoint=~\"$entrypoint\",code=\"200\"}[5m])",
"format": "time_series",
"intervalFactor": 2,
"legendFormat": "{{method}} : {{code}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Status code 200 over 5min",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": true,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 7,
"w": 12,
"x": 12,
"y": 16
},
"hiddenSeries": false,
"id": 6,
"legend": {
"alignAsTable": true,
"avg": false,
"current": true,
"max": true,
"min": true,
"rightSide": true,
"show": true,
"total": false,
"values": true
},
"lines": false,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "7.5.5",
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": true,
"steppedLine": false,
"targets": [
{
"expr": "rate(traefik_entrypoint_requests_total{entrypoint=~\"$entrypoint\",code!=\"200\"}[5m])",
"format": "time_series",
"intervalFactor": 2,
"legendFormat": "{{ method }} : {{code}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Others status code over 5min",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"cacheTimeout": null,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"decimals": 0,
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 0,
"y": 23
},
"id": 7,
"interval": null,
"links": [],
"maxDataPoints": 3,
"options": {
"displayLabels": [],
"legend": {
"calcs": [],
"displayMode": "table",
"placement": "right",
"values": ["value"]
},
"pieType": "pie",
"reduceOptions": {
"calcs": ["sum"],
"fields": "",
"values": false
},
"text": {}
},
"targets": [
{
"exemplar": true,
"expr": "sum(rate(traefik_service_requests_total[5m])) by (service) ",
"format": "time_series",
"interval": "",
"intervalFactor": 2,
"legendFormat": "{{ service }}",
"refId": "A"
}
],
"title": "Requests by service",
"type": "piechart"
},
{
"cacheTimeout": null,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"decimals": 0,
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 12,
"y": 23
},
"id": 8,
"interval": null,
"links": [],
"maxDataPoints": 3,
"options": {
"displayLabels": [],
"legend": {
"calcs": [],
"displayMode": "table",
"placement": "right",
"values": ["value"]
},
"pieType": "pie",
"reduceOptions": {
"calcs": ["sum"],
"fields": "",
"values": false
},
"text": {}
},
"targets": [
{
"exemplar": true,
"expr": "sum(rate(traefik_entrypoint_requests_total{entrypoint =~ \"$entrypoint\"}[5m])) by (entrypoint) ",
"format": "time_series",
"interval": "",
"intervalFactor": 2,
"legendFormat": "{{ entrypoint }}",
"refId": "A"
}
],
"title": "Requests by protocol",
"type": "piechart"
}
],
"schemaVersion": 27,
"style": "dark",
"tags": ["traefik", "prometheus"],
"templating": {
"list": [
{
"allValue": null,
"current": {},
"datasource": "Prometheus",
"definition": "label_values(service)",
"description": null,
"error": null,
"hide": 0,
"includeAll": false,
"label": null,
"multi": false,
"name": "service",
"options": [],
"query": {
"query": "label_values(service)",
"refId": "StandardVariableQuery"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
},
{
"allValue": null,
"current": {},
"datasource": "Prometheus",
"definition": "",
"description": null,
"error": null,
"hide": 0,
"includeAll": true,
"label": null,
"multi": true,
"name": "entrypoint",
"options": [],
"query": {
"query": "label_values(entrypoint)",
"refId": "Prometheus-entrypoint-Variable-Query"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
}
]
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
},
"timezone": "",
"title": "Traefik",
"uid": "qPdAviJmz",
"version": 10
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,94 @@
{ config, pkgs, modulesPath, lib, ... }:
{
services.grafana.enable = true;
services.grafana.settings.server = {
http_port = 3000;
http_addr = "0.0.0.0";
# Grafana needs to know on which domain and URL it's running
# domain = "monitor.local";
# root_url = "https://monitor.local/grafana/"; # Not needed if it is `https://your.domain/`
# serve_from_sub_path = true;
};
networking.firewall.allowedTCPPorts = [ 3000 ];
services.grafana = {
# declarativePlugins = with pkgs.grafanaPlugins; [ ... ];
provision = {
enable = true;
datasources.settings.datasources = [
# "Built-in" datasources can be provisioned - c.f. https://grafana.com/docs/grafana/latest/administration/provisioning/#data-sources
{
name = "Prometheus";
type = "prometheus";
url = "http://127.0.0.1:${toString config.services.prometheus.port}";
}
{
name = "Loki";
type = "loki";
url = "http://127.0.0.1:${toString config.services.loki.configuration.server.http_listen_port}";
}
# Some plugins also can - c.f. https://grafana.com/docs/plugins/yesoreyeram-infinity-datasource/latest/setup/provisioning/
# {
# name = "Infinity";
# type = "yesoreyeram-infinity-datasource";
# }
# But not all - c.f. https://github.com/fr-ser/grafana-sqlite-datasource/issues/141
];
# Note: removing attributes from the above `datasources.settings.datasources` is not enough for them to be deleted on `grafana`;
# One needs to use the following option:
# datasources.settings.deleteDatasources = [ { name = "foo"; orgId = 1; } { name = "bar"; orgId = 1; } ];
dashboards.settings.providers = [{
name = "my dashboards";
options.path = "/etc/grafana-dashboards";
}];
};
};
environment.etc."grafana-dashboards/traefik.json" = {
source = ./dashboards/traefik.json;
user = "grafana";
group = "grafana";
mode = "0644";
};
environment.etc."grafana-dashboards/grafana-traefik.json" = {
source = ./dashboards/grafana-traefik.json;
user = "grafana";
group = "grafana";
mode = "0644";
};
environment.etc."grafana-dashboards/node-exporter.json" = {
source = ./dashboards/node-exporter.json;
user = "grafana";
group = "grafana";
mode = "0644";
};
environment.etc."grafana-dashboards/promtail.json" = {
source = ./dashboards/promtail.json;
user = "grafana";
group = "grafana";
mode = "0644";
};
environment.etc."grafana-dashboards/gitea.json" = {
source = ./dashboards/gitea.json;
user = "grafana";
group = "grafana";
mode = "0644";
};
environment.etc."grafana-dashboards/postgres.json" = {
source = ./dashboards/postgres.json;
user = "grafana";
group = "grafana";
mode = "0644";
};
}

View file

@ -0,0 +1,11 @@
{ config, pkgs, modulesPath, lib, ... }:
{
imports = [
../../templates/base.nix
./networking.nix
./prometheus.nix
./grafana.nix
./loki.nix
];
}

View file

@ -0,0 +1,37 @@
{
networking.firewall.allowedTCPPorts = [ 3100 ];
services.loki = {
enable = true;
configuration = {
server.http_listen_port = 3100;
auth_enabled = false;
analytics.reporting_enabled = false;
common = {
ring = {
instance_addr = "127.0.0.1";
kvstore.store = "inmemory";
};
replication_factor = 1;
path_prefix = "/tmp/loki";
};
schema_config = {
configs = [
{
from = "2020-05-15";
store = "tsdb";
object_store = "filesystem";
schema = "v13";
index = {
prefix = "index_";
period = "24h";
};
}
];
};
storage_config.filesystem.directory = "/var/lib/loki/chunk";
};
};
}

View file

@ -0,0 +1,17 @@
{ config, lib, pkgs, ... }: {
networking = {
hostName = "monitor";
# interfaces.eth0 = {
# ipv4.addresses = [{
# address = "192.168.1.171";
# prefixLength = 24;
# }];
# };
# firewall.allowedTCPPorts = [ 80 3000 9090 ];
# defaultGateway = {
# address = "192.168.1.1";
# interface = "eth0";
# };
};
}

View file

@ -0,0 +1,70 @@
{ config, pkgs, modulesPath, lib, ... }:
let
monitor_ip = "monitor.local";
traefik_ip = "traefik.local";
sandbox_ip = "sandbox.local";
forgejo_ip = "forgejo.local";
prometheus_exporter_port = 9100;
promtail_port = 9080;
traefik_monitor_port = 8082;
forgejo_monitor_port = 3000;
in {
networking.firewall.allowedTCPPorts = [ 9090 ];
services.prometheus = {
enable = true;
retentionTime = "7d";
globalConfig = {
scrape_timeout = "10s";
scrape_interval = "30s";
};
scrapeConfigs = [
{
job_name = "node";
static_configs = [
{
targets = [
"${monitor_ip}:${toString prometheus_exporter_port}"
"${traefik_ip}:${toString prometheus_exporter_port}"
"${sandbox_ip}:${toString prometheus_exporter_port}"
"${forgejo_ip}:${toString prometheus_exporter_port}"
];
}
];
}
{
job_name = "traefik";
static_configs = [
{ targets = [ "${traefik_ip}:${toString traefik_monitor_port}" ]; }
];
}
{
job_name = "gitea";
static_configs = [
{ targets = [ "${forgejo_ip}:${toString forgejo_monitor_port}" ]; }
];
}
{
job_name = "postgres";
static_configs = [
{ targets = [ "${forgejo_ip}:9187" ]; }
];
}
{
job_name = "promtail";
static_configs = [
{
targets = [
"${monitor_ip}:${toString promtail_port}"
"${traefik_ip}:${toString promtail_port}"
"${sandbox_ip}:${toString promtail_port}"
"${forgejo_ip}:${toString promtail_port}"
];
}
];
}
];
};
}

View file

@ -0,0 +1,10 @@
{ config, pkgs, modulesPath, lib, ... }:
{
imports = [
../../templates/base.nix
./networking.nix
./storage.nix
./sandbox.nix
];
}

View file

@ -0,0 +1,20 @@
{ config, lib, pkgs, ... }: {
networking = {
hostName = "sandbox";
interfaces.eth0 = {
ipv4.addresses = [{
address = "192.168.1.148";
prefixLength = 24;
}];
ipv6.addresses = [{
address = "fe80::148";
prefixLength = 64;
}];
};
defaultGateway = {
address = "192.168.1.1";
interface = "eth0";
};
};
}

View file

@ -0,0 +1,4 @@
{ config, pkgs, modulesPath, lib, ... }:
{
}

View file

@ -0,0 +1,11 @@
{
boot.supportedFilesystems = [ "nfs" ];
services.rpcbind.enable = true;
fileSystems."/mnt/nas" = {
device = "192.168.1.226:/volume1/docker";
fsType = "nfs";
options = [ "noatime" "vers=4" "rsize=8192" "wsize=8192" ];
};
}

View file

@ -0,0 +1,10 @@
{ config, pkgs, modulesPath, lib, ... }:
{
imports = [
../../templates/base.nix
./networking.nix
./traefik.nix
./promtail.nix
];
}

View file

@ -0,0 +1,18 @@
{ config, lib, pkgs, ... }: {
networking = {
hostName = "traefik";
interfaces.eth0 = {
ipv4.addresses = [{
address = "192.168.1.171";
prefixLength = 24;
}];
};
firewall.allowedTCPPorts = [ 80 443 8080 8082 ];
defaultGateway = {
address = "192.168.1.1";
interface = "eth0";
};
};
}

View file

@ -0,0 +1,27 @@
{ config, lib, pkgs, ... }:
{
# This ensures the directory exists at boot, owned by traefik (writer) and readable by promtail.
systemd.tmpfiles.rules = [
"d /var/log/traefik 0755 traefik promtail -"
];
services.promtail.configuration.scrape_configs = lib.mkAfter [
{
job_name = "traefik";
static_configs = [
{
targets = [ "localhost" ];
labels = {
job = "traefik";
host = config.networking.hostName;
env = "proxmox";
instance = "${config.networking.hostName}.local"; # prometheus scrape target
__path__ = "/var/log/traefik/*.log";
};
}
];
}
];
}

View file

@ -0,0 +1,158 @@
{ config, lib, pkgs, ... }: {
# Traefik reverse proxy setup
services.traefik = {
enable = true;
staticConfigOptions = {
entryPoints = {
web = {
address = ":80";
asDefault = true;
http.redirections.entrypoint = {
to = "websecure";
scheme = "https";
};
};
websecure = {
address = ":443";
asDefault = true;
http.tls.certResolver = "letsencrypt";
};
metrics = {
address = ":8082";
};
};
api.dashboard = true;
api.insecure = true;
# Enable Let's Encrypt
certificatesResolvers = {
letsencrypt = {
acme = {
email = "david.mikael@proton.me"; # Replace with your email
storage = "/var/lib/traefik/acme.json"; # Location to store ACME certificates
httpChallenge = {
entryPoint = "web"; # Uses HTTP challenge (can also use DNS)
};
# Uncomment the following for staging (testing) environment
# caServer = "https://acme-staging-v02.api.letsencrypt.org/directory";
};
};
};
# Enable Prometheus metrics
metrics = {
prometheus = {
entryPoint = "metrics";
};
};
log = {
level = "DEBUG";
filePath = "/var/log/traefik/traefik.log";
};
accessLog = {
format = "json";
filePath = "/var/log/traefik/access.log";
};
# Enable access logs (you can customize the log format)
# accessLog = {
# filePath = "/var/log/traefik/access.log"; # Log to a file
# format = "common"; # You can adjust this to `json` or `common`
# };
# tracing = {
# enabled = true;
# provider = "jaeger"; # or zipkin, or other
# jaeger = {
# apiURL = "http://localhost:5775"; # Replace with your Jaeger instance URL
# };
# };
};
dynamicConfigOptions = {
# Add IP whitelisting middleware to restrict access to internal network only
http.middlewares = {
internal-whitelist = {
ipWhiteList = {
sourceRange = ["192.168.1.0/24"]; # Adjust to your internal network range
# Alternatively use `127.0.0.1/32` for localhost access
};
};
};
# Route to Proxmox UI
http.routers.proxmox = {
rule = "Host(`proxmox.procopius.dk`)";
service = "proxmox";
entryPoints = [ "web" "websecure" ];
tls = {
certResolver = "letsencrypt"; # Use Let's Encrypt
};
};
# Route to Traefik Dashboard
http.routers.traefik = {
rule = "Host(`traefik.procopius.dk`)";
service = "traefik";
entryPoints = [ "web" "websecure" ];
middlewares = ["internal-whitelist"];
tls = {
certResolver = "letsencrypt"; # Use Let's Encrypt
};
};
http.routers.forgejo = {
rule = "Host(`git.procopius.dk`)";
service = "forgejo";
entryPoints = [ "web" "websecure" ];
tls = {
certResolver = "letsencrypt"; # Use Let's Encrypt
};
};
# Route to Traefik Dashboard
http.routers.catchAll = {
# rule = "Host(`jellyfin.procopius.dk`)";
rule = "HostRegexp(`.+`)";
# rule = "HostRegexp(`{host:.+}`)";
service = "nginx";
entryPoints = [ "web" "websecure" ];
tls = {
certResolver = "letsencrypt"; # Use Let's Encrypt
};
};
# Define the services
http.services.proxmox.loadBalancer.servers = [
{ url = "https://192.168.1.205:8006"; } # Proxmox
];
http.services.proxmox.loadBalancer.serversTransport = "insecureTransport";
http.services.traefik.loadBalancer.servers = [
{ url = "http://traefik.local:8080"; } # Traefik Dashboard
];
http.services.forgejo.loadBalancer.servers = [
{ url = "http://192.168.1.249:3000"; } # forgejo
];
http.services.nginx.loadBalancer.servers = [
{ url = "https://192.168.1.226:4433"; } # nginx
];
http.services.nginx.loadBalancer.serversTransport = "insecureTransport";
http.serversTransports.insecureTransport.insecureSkipVerify = true;
};
};
# Optionally, you can add Docker support if using Docker Compose
virtualisation.docker.enable = true;
}

View file

@ -0,0 +1,77 @@
{ config, pkgs, lib, ... }:
let
# ── Adjust these to your NAS settings ──────────────────────────────────────────
nasServer = "192.168.1.100"; # your NAS IP or hostname
nasExportPath = "/export/docker-volumes"; # path on the NAS
nasMountPoint = "/mnt/nas"; # where to mount inside VM
# ── Where we drop your Compose file and run it ────────────────────────────────
composeDir = "/etc/docker-compose-app";
composeText = lib.readFile ./docker-compose.yml;
in {
##############################################################################
# A) NETWORKING
# (If you want DHCP, remove this block and let cloud-init assign an IP.)
##############################################################################
# networking.interfaces.enp0s25 = {
# useDHCP = false;
# ipv4.addresses = [{
# address = "192.168.1.50";
# prefixLength = 24;
# }];
# ipv4.gateway = "192.168.1.1";
# # optional: ipv4.dns = [ "1.1.1.1" "8.8.8.8" ];
# };
##############################################################################
# B) MOUNT YOUR NAS VIA NFS
##############################################################################
# fileSystems."${nasMountPoint}" = {
# device = "${nasServer}:${nasExportPath}";
# fsType = "nfs";
# options = [ "defaults" "nofail" "x-systemd.requires=network-online.target" ];
# };
# fileSystems."${nasMountPoint}".requiredForBoot = false;
##############################################################################
# C) INSTALL DOCKER & DOCKER-COMPOSE
##############################################################################
environment.systemPackages = with pkgs; [
docker
docker-compose
];
services.docker.enable = true;
##############################################################################
# D) DROP IN YOUR docker-compose.yml
##############################################################################
# systemd.tmpfiles.rules = [
# # Ensure directory exists before we write the file.
# "D! ${composeDir} 0755 root root - -"
# ];
# environment.etc."docker-compose-app/docker-compose.yml".text = composeText;
##############################################################################
# E) RUN DOCKER-COMPOSE AS A SYSTEMD SERVICE
##############################################################################
# systemd.services.dockerComposeApp = {
# description = "Auto-start Docker-Compose stack for home server";
# after = [ "network-online.target" "docker.service" ];
# wants = [ "network-online.target" "docker.service" ];
# serviceConfig = {
# WorkingDirectory = composeDir;
# ExecStart = "${pkgs.docker-compose}/bin/docker-compose -f ${composeDir}/docker-compose.yml up";
# ExecStop = "${pkgs.docker-compose}/bin/docker-compose -f ${composeDir}/docker-compose.yml down";
# Restart = "always";
# RestartSec = 10;
# };
# wantedBy = [ "multi-user.target" ];
# };
}

11
nixos/modules/docker.nix Normal file
View file

@ -0,0 +1,11 @@
{
config,
pkgs,
inputs,
...
}: {
virtualisation.docker = {
enable = true;
enableOnBoot = false;
};
}

54
nixos/modules/forgejo.nix Normal file
View file

@ -0,0 +1,54 @@
{ config, pkgs, ... }:
let
# (Optional) name your Compose apps directory on the VM:
composeDir = "/etc/docker-compose-app";
in {
# 1) Install Docker engine and DockerCompose binary:
environment.systemPackages = with pkgs; [
docker
docker-compose # pulls in the python-based compose
];
# 2) Enable the Docker daemon:
services.docker.enable = true;
# 3) Create a directory for your Compose file and copy it from the flake:
# If your flake repo has a sibling file `docker-compose.yml`, this will drop
# it into /etc/docker-compose-app/docker-compose.yml on the VM.
environment.etc."docker-compose-app/docker-compose.yml".text = builtins.readFile ./docker-compose.yml;
# 4) Make sure that directory exists with the right permissions:
systemd.tmpfiles.rules = [
# D = create directory if missing, mode 0755, owner root:root
"D! /etc/docker-compose-app 0755 root root - -"
];
# 5) Define a systemd service to run `docker-compose up`:
systemd.services.dockerComposeApp = {
description = "docker-compose stack for my application";
after = [ "network-online.target" "docker.service" ];
wants = [ "network-online.target" "docker.service" ];
serviceConfig = {
# Run in foreground but let systemd restart if it crashes
ExecStart = "${pkgs.docker-compose}/bin/docker-compose -f ${composeDir}/docker-compose.yml up";
ExecStop = "${pkgs.docker-compose}/bin/docker-compose -f ${composeDir}/docker-compose.yml down";
WorkingDirectory = composeDir;
Restart = "always";
RestartSec = 10;
};
# Make sure the directory exists before this service starts:
preStart = ''
mkdir -p ${composeDir}
chown root:root ${composeDir}
'';
wantedBy = [ "multi-user.target" ];
};
# 6) (Optional) If any volumes need to exist, define them here, for example:
# environment.etc."docker-compose-app/data".source = "/path/to/local/data";
}

View file

@ -0,0 +1,19 @@
{ config, pkgs, ... }:
let
prometheus_exporter_port = 9100;
in
{
networking.firewall.allowedTCPPorts = [ prometheus_exporter_port ];
services.prometheus = {
exporters = {
node = {
enable = true;
enabledCollectors = [ "systemd" ];
port = prometheus_exporter_port;
# /nix/store/zgsw0yx18v10xa58psanfabmg95nl2bb-node_exporter-1.8.1/bin/node_exporter --help
extraFlags = [ "--collector.ethtool" "--collector.softirqs" "--collector.tcpstat" "--collector.wifi" ];
};
};
};
}

View file

@ -0,0 +1,43 @@
{ config, pkgs, ... }:
let
promtail_port = 9080;
in
{
networking.firewall.allowedTCPPorts = [ promtail_port ];
systemd.tmpfiles.rules = [
"d /var/lib/promtail 0755 promtail promtail -"
];
services.promtail = {
enable = true;
configuration = {
server = {
http_listen_port = promtail_port;
grpc_listen_port = 0;
};
positions = {
filename = "/var/lib/promtail/positions.yaml";
};
clients = [{
url = "http://monitor.local:3100/loki/api/v1/push";
}];
scrape_configs = [{
job_name = "journal";
journal = {
path = "/var/log/journal";
labels = {
job = "promtail";
host = config.networking.hostName;
env = "proxmox";
instance = "${config.networking.hostName}.local";
};
};
relabel_configs = [{
source_labels = ["__journal__systemd_unit"];
target_label = "unit";
}];
}];
};
};
}

1
nixos/secrets/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
*.key

51
nixos/secrets/README.md Normal file
View file

@ -0,0 +1,51 @@
🔑 2. Generate an age Keypair
age-keygen -o secrets/age.key
This will output something like:
# created: 2025-06-02T22:00:00Z
# public key: age1abcdefghijk...
Copy that public key somewhere — youll need it for encrypting.
✅ You should now have:
secrets/
├── age.key # keep this safe and private!
📝 3. Create Encrypted Secrets File
sops --age age1abcdefghijk... secrets/secrets.yaml
This opens a YAML file in your $EDITOR. Add secrets like:
forgejo-admin-password: "my-super-secret-password"
Save and close the file — its now encrypted using the public key.
✅ Now you should have:
secrets/
├── age.key
├── secrets.yaml # encrypted file (safe to commit)
You can commit secrets.yaml, but do not commit age.key unless you're OK with putting it on a VM.
🧪 Test Decryption Locally
export SOPS_AGE_KEY_FILE=secrets/age.key
To test:
sops -d secrets/secrets.yaml
To edit:
sops secrets/secrets.yaml
[plasmagoat@forgejo:~]$ sudo chmod 400 /etc/sops/age.key && sudo chown root:root /etc/sops/age.key

View file

@ -0,0 +1,27 @@
forgejo-admin-password: ENC[AES256_GCM,data:cLC4JQC8PMF4/aeVBzOROupPLzd7TbYwvudr7yVx4YpLCGSmYXRwJQAoXg==,iv:tG2kL66ZshwZkJodZQ5K8SZKfG1eJYeX9eYsZ7yM7rA=,tag:0roW0M9eUmzejkH6pwN/IA==,type:str]
forgejo-db-password: ENC[AES256_GCM,data:0KZJHmNuxpO8TmLNuryipICPTjG9h56+II1Azk+v3fkE5MAb9g==,iv:zb14BvbC2OehCYATgMMoPXv742jjD4v0B12cVhNCWBw=,tag:pnrboj5IvwXYXaZJbZpxTQ==,type:str]
hello: ENC[AES256_GCM,data:XkOLnE2Mkunc0zNF1932jOuz1olAwWf56lkqL2dt+h99WoL/vNLfSQ0al8NfEA==,iv:WC2xbB9WmB/khOVjdClFerJ8kjtHjaR/p6rDYaaDZhY=,tag:tT92FNrRm74XoZxoFFXm5g==,type:str]
example_key: ENC[AES256_GCM,data:kBk87OXu+qfJjP/2EA==,iv:64WcHaVfQrVCouUCZoHk0z/4ii8U9m61/E9SqLeB3Ms=,tag:MZJ6m7m4+s6BNGhtNs+ZFQ==,type:str]
#ENC[AES256_GCM,data:lM4LNQNU2S66a73pUymyUA==,iv:pAHgR+ViSO3Ff2zSaZQcXNGb2r2KH+ZbRd33vpq8ncs=,tag:WTNQCjaESLXTXwcwZePU2A==,type:comment]
example_array:
- ENC[AES256_GCM,data:Sc1q0Yd3sQ6eOzSwfQA=,iv:L4YBbWWeQZAYROHpiNEtHLDCdcuW+vvEpYhGxD0b62g=,tag:82L6MlHWIMpxKb4B3+Lszg==,type:str]
- ENC[AES256_GCM,data:Ud9dpSAcHc8NOq48wQI=,iv:9ERTBUQqKHPUIG57KXbRPMXN37cx+WcxOCDxCWpbE1k=,tag:ftTGF/obIJVZSTodIGoABw==,type:str]
example_number: ENC[AES256_GCM,data:1Xvp578L4rjW6g==,iv:82z/MQM586y4WilPZgmisa2C7GTdG0vmIEkyx/aMCXw=,tag:UtNDNKbu0tuhSyu1OQiJJA==,type:float]
example_booleans:
- ENC[AES256_GCM,data:RkxG/g==,iv:RNZpV/1KRWOazIuHj+SH7r3AmwnRBIUgXgfDplrk5X0=,tag:cKv0dVJGQcluscNspIrPgg==,type:bool]
- ENC[AES256_GCM,data:PvghSeY=,iv:xPlMb1LMsg5gAWsCXT3UnMyOfQmSKDKdDrjt+n9+Nqs=,tag:B2aROAGdcupDmoOHAiXeTg==,type:bool]
sops:
age:
- recipient: age1n20y9kmdh324m3tkclvhmyuc7c8hk4w84zsal725adahwl8nzq0s04aq4y
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwVElvVXluZCsxK1BiT3c2
Zm9kaURNdnZ2Nk9EM0dld2tjdFhrZlFiSEVnCk8zZVpWWlFXS3JYS0Q2WHExLzFU
WkFwcDFmR3VrdHFmS2JmVC95TnZIMjQKLS0tIGsyVmp1Sm1uL3FKVWlERUZHdmVw
TG9HYXdUdlZNYXJUZng2ejBwbjJoNVkK0ER6mqLdz0hEaovWME4p56tjuYbPIuhb
X1smwLmHxgcRboeFU5dyp3wZKBg7ccRPneQKsgJvYb929BesynHr6g==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2025-06-03T16:03:32Z"
mac: ENC[AES256_GCM,data:mLCtH1EPm1cD7KD/fCVO0hrIfG6AOl396kcwdahyr326IRvTneT+6lr+f0XAHSkPXtRsmSCiD9WNhLYAh/kCfsP7tVPKl4X17OHkK9blUJ5JpuqnZJfOQ3PXNitYFvcSUUi1Y1/vIQmDf52oTPlcZgxmTgsQj4MEJIIni7d0SOc=,iv:MhAJ0QAdyHv8BzHIBQ/lZ7zV/MKjcsicbBOw9kwo7Nc=,tag:qrfTfCPxAMvXOm69BMWJ4g==,type:str]
unencrypted_suffix: _unencrypted
version: 3.10.2

8
nixos/secrets/sops.nix Normal file
View file

@ -0,0 +1,8 @@
{ config, lib, ... }:
{
sops = {
defaultSopsFile = ./secrets.yaml;
age.keyFile = "/etc/sops/age.key";
#secrets."forgejo-admin-password".owner = "forgejo";
};
}

11
nixos/templates/base.nix Normal file
View file

@ -0,0 +1,11 @@
{ config, pkgs, modulesPath, lib, ... }:
{
# Pull in all the shared settings from configuration.nix
imports = [
../configuration.nix
../modules/node-exporter.nix
../modules/promtail.nix
../users/plasmagoat.nix
];
}

View file

@ -0,0 +1,15 @@
{ config, pkgs, modulesPath, lib, ... }:
{
# Pull in all the shared settings from configuration.nix
imports = [
./base.nix
];
config = {
environment.systemPackages = with pkgs; [
docker
docker-compose
];
};
}

View file

@ -0,0 +1,29 @@
{ config, lib, pkgs, ... }: {
users.users.plasmagoat = {
isNormalUser = true;
description = "plasmagoat";
extraGroups = [
"networkmanager"
"wheel"
"docker"
];
# shell = pkgs.zsh;
openssh.authorizedKeys.keys = [
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCeg/n/vst9KME8byhxX2FhA+FZNQ60W38kkNt45eNzK5zFqBYuwo1nDXVanJSh9unRvB13b+ygpZhrb4sHvkETGWiEioc49MiWr8czEhu6Wpo0vv5MAJkiYvGZUYPdUW52jUzWcYdw8PukG2rowrxL5G0CmsqLwHMPU2FyeCe5aByFI/JZb8R80LoEacgjUiipJcoLWUVgG2koMomHClqGu+16kB8nL5Ja3Kc9lgLfDK7L0A5R8JXhCjrlEsmXbxZmwDKuxvjDAZdE9Sl1VZmMDfWkyrRlenrt01eR3t3Fec6ziRm5ZJk9e2Iu1DPoz+PoHH9aZGVwmlvvnr/gMF3OILxcqb0qx+AYlCCnb6D6pJ9zufhZkKcPRS1Q187F6fz+v2oD1xLZWFHJ92+7ItM0WmbDOHOC29s5EA6wNm3iXZCq86OI3n6T34njDtPqh6Z7Pk2sdK4GBwnFj4KwEWXvdKZKSX1qb2EVlEBE9QI4Gf3eg4SiBu2cAFt3nOSzs8c= asol\dbs@ALPHA-DBS-P14sG2"
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+U3DWOrklcA8n8wdbLBGyli5LsJI3dpL2Zod8mx8eOdC4H127ZT1hzuk2uSmkic4c73BykPyQv8rcqwaRGW94xdMRanKmHYxnbHXo5FBiGrCkNlNNZuahthAGO49c6sUhJMq0eLhYOoFWjtf15sr5Zu7Ug2YTUL3HXB1o9PZ3c9sqYHo2rC/Il1x2j3jNAMKST/qUZYySvdfNJEeQhMbQcdoKJsShcE3oGRL6DFBoV/mjJAJ+wuDhGLDnqi79nQjYfbYja1xKcrKX+D3MfkFxFl6ZIzomR1t75AnZ+09oaWcv1J7ehZ3h9PpDBFNXvzyLwDBMNS+UYcH6SyFjkUbF David@NZXT"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICUP7m8jZJiclZGfSje8CeBYFhX10SrdtjYziuChmj1X plasmagoat@macbook-air"
];
};
users.motd = with config; ''
Welcome to ${networking.hostName}
- This server is managed by NixOS
- Admin: plasmagoat
OS: NixOS ${system.nixos.release} (${system.nixos.codeName})
Version: ${system.nixos.version}
Kernel: ${boot.kernelPackages.kernel.version}
'';
}