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

23
README.md Normal file
View file

@ -0,0 +1,23 @@
Create a VMA image form vm-profile.nix
```nixos-generate -f proxmox -c vm/vm-profile.nix```
Upload to proxmox server
```scp ./vzdump-qemu-nixos-25.11pre-git.vma.zst root@192.168.1.205:/var/lib/vz/dump/```
```scp /nix/store/y7aff8xdwqwmq349jr2yzrx8wrf0vrvj-proxmox-nixos-25.11pre-git/vzdump-qemu-nixos-25.11pre-git.vma.zst root@192.168.1.205:/var/lib/vz/dump/```
```ssh root@192.168.1.205```
```qmrestore /var/lib/vz/dump/vzdump-qemu-nixos-25.11pre-git.vma.zst 200 --unique true```
vma extract /var/lib/vz/template/backup/vzdump-qemu-nixos-25.11pre-git.vma.zst /var/lib/vz/images/200
qm start 200
ssh-copy-id root@192.168.1.205
ssh root@192.168.1.205
zstd -d /var/lib/vz/template/backup/vzdump-qemu-nixos-25.11pre-git.vma.zst -o /var/lib/vz/template/backup/vzdump-qemu-nixos-25.11pre-git.vma

3
ansible/README.me Normal file
View file

@ -0,0 +1,3 @@
ansible-playbook -i inventory.yml books/bootstrap/bootstrap-proxmox.yml
ansible-playbook -i inventory.yml books/upload-nix-image.yml

3
ansible/ansible.cfg Normal file
View file

@ -0,0 +1,3 @@
[defaults]
roles_path = ./roles
inventory = ./inventory.yml

View file

@ -0,0 +1,40 @@
- name: Bootstrap Proxmox Server
hosts: proxmox
become: true
pre_tasks:
- name: Remove enterprise repository
ansible.builtin.apt_repository:
update_cache: false
repo: deb https://enterprise.proxmox.com/debian/pve bookworm pve-enterprise
state: absent
- name: Remove enterprise pbs repository
ansible.builtin.apt_repository:
update_cache: false
repo: deb https://enterprise.proxmox.com/debian/pbs bookworm InRelease
state: absent
- name: Remove enterprise ceph repository
ansible.builtin.apt_repository:
update_cache: false
repo: deb https://enterprise.proxmox.com/debian/ceph-quincy bookworm enterprise
state: absent
- name: Add community repository
ansible.builtin.apt_repository:
update_cache: true
repo: deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription
state: present
tasks:
- name: Ensure ethtool offload post-up is present for eno1
ansible.builtin.lineinfile:
path: /etc/network/interfaces
line: "\tpost-up ethtool -K eno2 tso off gso off"
insertafter: "^iface eno2 inet manual"
state: present
backup: yes
- import_tasks: ../tasks/packages.yml
# - import_tasks: ../tasks/locale.yml
# - import_tasks: ../tasks/keyboard.yml
# - import_tasks: ../tasks/users.yml
# - import_tasks: ../tasks/ssh.yml

View file

@ -0,0 +1,82 @@
- name: Clone VM from Template
hosts: proxmox
become: true
vars_prompt:
- name: "template_vmid"
prompt: "Enter VMID for the template"
private: no
default: "9000"
- name: "new_vmid"
prompt: "Enter the new VMID"
private: no
default: "9001"
- name: "new_name"
prompt: "Enter name for the new VM"
private: no
default: "nixos-clone"
vars:
storage: "local-lvm"
tasks:
- name: Clone the template
ansible.builtin.command: >
qm clone {{ template_vmid }} {{ new_vmid }} --name {{ new_name }}
- name: Resize disk to 10G
ansible.builtin.command: >
qm resize {{ new_vmid }} virtio0 +5G
- name: Set cloud-init params
ansible.builtin.command: >
qm set {{ new_vmid }}
--ciuser root
--cipassword root
--sshkey /root/.ssh/id_rsa.pub
--ipconfig0 ip=dhcp
- name: Start the new VM
ansible.builtin.command: >
qm start {{ new_vmid }}
- name: Wait for QEMU Guest Agent to come online
retries: 20
delay: 5
ansible.builtin.shell: |
qm guest exec {{ new_vmid }} -- true
register: qga_check
until: qga_check.rc == 0
- name: Get IP addresses via QEMU Guest Agent
ansible.builtin.shell: |
qm guest cmd {{ new_vmid }} network-get-interfaces
register: qga_json
failed_when: qga_json.rc != 0
- name: Parse out eth0s IPv4 address
ansible.builtin.set_fact:
vm_ipv4: >-
{{
(
qga_json.stdout
| from_json
| selectattr('name','equalto','eth0')
| map(attribute='ip-addresses')
| first
| selectattr('ip-address-type','equalto','ipv4')
| map(attribute='ip-address')
| first
)
}}
- name: Show the VMs IP
ansible.builtin.debug:
msg: "VM {{ new_vmid }} ({{ new_name }}) reports IPv4: {{ vm_ipv4 }}"
# - name: Add new VMs IP to in-memory inventory (for later tasks)
# ansible.builtin.add_host:
# name: "nixos-{{ new_vmid }}"
# ansible_host: "{{ vm_ipv4 }}"
# ansible_user: root

View file

@ -0,0 +1,81 @@
- name: Build and Upload NixOS Image, Restore and Convert to Template
hosts: nodes
gather_facts: false
vars:
flake_name: "{{ flake | default('base') }}"
image_dir: "{{ playbook_dir }}/../../nixos"
result_path: "{{ image_dir }}/result"
dest_dir: "/var/lib/vz/dump/"
tasks:
- name: Build NixOS base image
ansible.builtin.shell: nix build .#{{ flake_name }}
args:
chdir: "{{ image_dir }}"
register: build_result
delegate_to: localhost
- name: Get built image file
ansible.builtin.find:
paths: "{{ result_path }}"
patterns: "*.vma.zst"
register: built_image
delegate_to: localhost
- name: Fail if no image was built
ansible.builtin.fail:
msg: "No image file found in result/"
when: built_image.files | length == 0
delegate_to: localhost
- name: Set fact for built image path
ansible.builtin.set_fact:
local_image_path: "{{ built_image.files[0].path | realpath }}"
image_filename: "{{ built_image.files[0].path | basename }}"
delegate_to: localhost
- name: Copy image to Proxmox
ansible.builtin.copy:
src: "{{ local_image_path }}"
dest: "{{ dest_dir }}"
- name: Remove local build result
ansible.builtin.file:
path: "{{ result_path }}"
state: absent
delegate_to: localhost
- name: Restore and Convert to Template on Proxmox
hosts: nodes
become: true
vars_prompt:
- name: "vmid"
prompt: "Enter VMID for the template"
private: no
default: "9000"
- name: "vmname"
prompt: "Enter name for the Proxmox template"
private: no
default: "nixos-base"
vars:
image_path: "/var/lib/vz/dump/{{ image_filename }}"
cpu_cores: 2
memory_mb: 2048
tasks:
- name: Restore VM from image
ansible.builtin.command: >
qmrestore {{ image_path }} {{ vmid }} --unique true
args:
creates: "/etc/pve/qemu-server/{{ vmid }}.conf"
- name: Set name, CPU and memory
ansible.builtin.command: >
qm set {{ vmid }} --cores {{ cpu_cores }} --memory {{ memory_mb }} --name {{ vmname }}
- name: Convert VM to template
ansible.builtin.command: qm template {{ vmid }}

View file

@ -0,0 +1,13 @@
- name: Install Nix (multi-user mode)
ansible.builtin.shell: |
if [ ! -x /nix/var/nix/profiles/default/bin/nix ]; then
curl -L https://nixos.org/nix/install | bash -s -- --daemon
fi
args:
creates: /nix
- name: Enable flakes in nix config
ansible.builtin.copy:
dest: /etc/nix/nix.conf
content: |
experimental-features = nix-command flakes

View file

@ -0,0 +1,25 @@
- name: Stop nix-daemon service
ansible.builtin.systemd:
name: nix-daemon.service
state: stopped
enabled: no
ignore_errors: yes
- name: Remove /nix directory
ansible.builtin.file:
path: /nix
state: absent
- name: Remove nix entries from root's shell config
ansible.builtin.lineinfile:
path: /root/.bashrc
regexp: "^.*nix.*$"
state: absent
ignore_errors: yes
- name: Remove nix entries from root's shell profile
ansible.builtin.lineinfile:
path: /root/.profile
regexp: "^.*nix.*$"
state: absent
ignore_errors: yes

View file

@ -0,0 +1,17 @@
- name: Ensure NFS subfolders exist on NAS
hosts: nas
become: true
tasks:
- name: Ensure /volume1/data exists
file:
path: /volume1/data
state: directory
mode: "0755"
- name: Ensure /volume1/data/forgejo exists
file:
path: /volume1/data/forgejo
state: directory
owner: 992
group: 990
mode: "0770"

View file

@ -0,0 +1,2 @@
- name: Set keyboard layout
ansible.builtin.command: localectl set-keymap dk

View file

@ -0,0 +1,10 @@
- name: Ensure locale is set
ansible.builtin.locale_gen:
name: "en_DK.UTF-8"
state: present
- name: Set system locale
ansible.builtin.command: localectl set-locale LANG=en_DK.UTF-8
- name: Set timezone
ansible.builtin.command: timedatectl set-timezone Europe/Copenhagen

View file

@ -0,0 +1,8 @@
- name: Install required packages
ansible.builtin.apt:
name:
- python3
- curl
- git
state: present
update_cache: yes

View file

@ -0,0 +1,14 @@
- name: Ensure user plasmagoat exists
ansible.builtin.user:
name: plasmagoat
groups: wheel,docker,networkmanager
shell: /usr/bin/zsh
state: present
- name: Set authorized SSH keys for plasmagoat
ansible.posix.authorized_key:
user: plasmagoat
key: "{{ item }}"
loop:
- "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCeg/n/..."
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICUP7m8jZJ..."

View file

@ -0,0 +1,16 @@
- name: Disable PasswordAuthentication in sshd_config
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: "^PasswordAuthentication"
line: "PasswordAuthentication no"
- name: Disable KbdInteractiveAuthentication in sshd_config
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: "^KbdInteractiveAuthentication"
line: "KbdInteractiveAuthentication no"
- name: Restart sshd to apply changes
ansible.builtin.service:
name: ssh
state: restarted

View file

@ -0,0 +1,8 @@
---
- name: Upgrade
hosts: nodes
tasks:
- name: Upgrade
tags: upgrade
ansible.builtin.apt:
upgrade: dist

View file

@ -0,0 +1,6 @@
- name: Deploy and bootstrap mailer VM
hosts: mailer
become: true
roles:
- common

View file

@ -0,0 +1,26 @@
- name: Build and Upload NixOS Image
hosts: nodes
vars:
current_date: "{{ ansible_date_time.date }}"
flake_name: "{{ flake | default('proxmox') }}"
tasks:
- name: create iso
ansible.builtin.shell: nix build .#{{ flake_name }}
args:
chdir: "{{ playbook_dir }}/../../nixos/nixos-base-image"
delegate_to: localhost
run_once: true
- name: copy the backup
ansible.builtin.copy:
src: "{{ item }}"
dest: "/var/lib/vz/dump/"
with_fileglob:
- "{{ playbook_dir }}/../../nixos/nixos-base-image/result/*.zst"
- name: remove build result
ansible.builtin.file:
path: "{{ playbook_dir }}/../../nixos/nixos-base-image/result"
state: absent
delegate_to: localhost
run_once: true

28
ansible/inventory.yml Normal file
View file

@ -0,0 +1,28 @@
nodes:
hosts:
proxmox-01:
ansible_host: 192.168.1.205
ansible_user: root
ansible_ssh_private_key_file: ~/.ssh/id_ed25519
vms:
hosts:
mailer:
ansible_host: 192.168.1.36
ansible_user: plasmagoat
nas:
hosts:
nas-01:
ansible_host: 192.168.1.226
ansible_user: plasmagoat
ansible_ssh_private_key_file: ~/.ssh/id_ed25519
proxmox:
hosts:
proxmox-01:
# vms:
# hosts:
# tailgate:
# ansible_user: example
# ansible_host: example.example.ts.net

1
ansible/roles/common/files/.gitignore vendored Normal file
View file

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

View file

@ -0,0 +1,15 @@
- name: Ensure /etc/sops exists
file:
path: /etc/sops
state: directory
owner: root
group: root
mode: "0700"
- name: Upload age key
copy:
src: files/age.key
dest: /etc/sops/age.key
owner: root
group: root
mode: "0600"

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}
'';
}