Compare commits

...

10 Commits

Author SHA1 Message Date
danlin 7f1767345a Remove dj-beets project
No longer in use. Drops the beets containers (cli/web), build +
autoimport services and timer, the beet CLI wrapper, /opt/dj-beets
tmpfiles entry, firewall port 8337, the Makefile copy step, and the
src/dj-beets tree (which contained a Beatport token) so the repo can
be made public safely.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 19:50:03 +02:00
danlin ad1dd816f4 Add LAN-only music vhost and enable weekly autoUpgrade
- gateway: replace konnektor/api/auth/mailpit vhosts with
  music.home.lindenfelser.de -> 10.202.82.195:80, LAN-only (403 otherwise)
- common: enable system.autoUpgrade, pulling config from Gitea main and
  overriding nixpkgs to the nixos-25.11 tip for weekly package refresh;
  reboot allowed in a 04:00-06:00 window

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 18:51:02 +02:00
danlin a0658b3a97 Merge branch 'claude/stoic-edison-841399'
* Remove k8s-server host
* Clean up duplicate sections in README
* Track latest gitea release
* Add konnektor + api/auth/mailpit reverse proxies
* Point konnektor.home at konnektor-web

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:58:42 +02:00
danlin cb0c3c791b Persist filebrowser config to /data/filebrowser
Mount /data/filebrowser as /config and point FB_DATABASE at
/config/filebrowser.db so the user/share database survives
container recreations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:58:29 +02:00
danlin 7568578f60 Point konnektor.home.lindenfelser.de at konnektor-web
konnektor-web is deployed and listening on 10.202.82.49:8082.
Replace the 503 placeholder with a real reverse_proxy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:52:35 +02:00
danlin bb30e1ab3c Expand konnektor vhosts: api, auth, mailpit
Repoint konnektor.home.lindenfelser.de to a 503 placeholder
until konnektor-web is built. Add api.* and auth.* subdomains
pointing at konnektor-api (8080) and Zitadel (8081); auth
forwards Host header so Zitadel routes correctly. Add
mailpit.* restricted to LAN 10.202.82.0/24.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 23:53:46 +02:00
danlin 96c16b6f52 Add konnektor reverse proxy to gateway
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 22:26:24 +02:00
danlin c8ccc76173 Track latest gitea release instead of pinning to 1.25
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:50:03 +02:00
danlin 5f1b06da70 Remove duplicate sections from README
The rclone, SMB password and Troubleshooting sections were
present twice in the file. Drop the second copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:35:52 +02:00
danlin d9a1dde5a5 Remove k8s-server host
Drop k8s-server from flake, Makefile targets, modules
(kubernetes.nix, docker-registry.nix), install script and
hardware config. Remove related registry vhost, port 6443
and NAT rules from gateway.nix. Drop K8s-Server section
and dangling markdown wrapper from README.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:29:46 +02:00
21 changed files with 38 additions and 2623 deletions
+2 -51
View File
@@ -1,9 +1,8 @@
.PHONY: help deploy deploy-gateway deploy-fileserver deploy-k8s-server install-gateway install-fileserver install-k8s-server fetch-hwconfig-gateway fetch-hwconfig-fileserver fetch-hwconfig-k8s-server upgrade-containers all
.PHONY: help deploy deploy-gateway deploy-fileserver install-gateway install-fileserver fetch-hwconfig-gateway fetch-hwconfig-fileserver upgrade-containers all
# Hosts
GATEWAY_HOST := danlin@10.202.82.3
FILESERVER_HOST := danlin@10.202.82.6
K8S_HOST := danlin@10.202.82.7
# Default target
all: deploy
@@ -15,30 +14,16 @@ help:
@echo " make deploy Deploy to gateway and fileserver"
@echo " make deploy-gateway Deploy to gateway only"
@echo " make deploy-fileserver Deploy to fileserver only"
@echo " make deploy-k8s-server Deploy to k8s-server only"
@echo " make upgrade-containers Pull latest images and restart containers on fileserver"
@echo " make install-gateway Run installation script for gateway"
@echo " make install-fileserver Run installation script for fileserver"
@echo " make install-k8s-server Run installation script for k8s-server"
@echo " make fetch-hwconfig-gateway Fetch hardware config from gateway"
@echo " make fetch-hwconfig-fileserver Fetch hardware config from fileserver"
@echo " make fetch-hwconfig-k8s-server Fetch hardware config from k8s-server"
@echo ""
# Deployment targets
deploy: deploy-gateway deploy-fileserver
deploy-k8s-server:
@echo "======================================"
@echo "Deploying k8s-server..."
@echo "======================================"
@echo "Syncing files to k8s-server..."
rsync -av --delete --exclude '.git' ./ $(K8S_HOST):/tmp/infra-nix/
@echo "Building and switching on k8s-server..."
ssh -tt $(K8S_HOST) "cd /tmp/infra-nix && sudo nixos-rebuild switch --flake '.#k8s-server'"
@echo "✓ k8s-server deployed successfully"
@echo ""
deploy-gateway:
@echo "======================================"
@echo "Deploying gateway..."
@@ -55,11 +40,7 @@ deploy-fileserver:
@echo "Deploying fileserver..."
@echo "======================================"
@echo "Syncing files to fileserver..."
rsync -av --delete --exclude '.git' --exclude 'src/dj-beets/poetry.lock' ./ $(FILESERVER_HOST):/tmp/infra-nix/
@echo "Copying dj-beets project to /opt/dj-beets..."
ssh $(FILESERVER_HOST) "sudo mkdir -p /opt/dj-beets"
rsync -av --delete src/dj-beets/ $(FILESERVER_HOST):/tmp/dj-beets-tmp/
ssh $(FILESERVER_HOST) "sudo rsync -a --delete /tmp/dj-beets-tmp/ /opt/dj-beets/ && sudo rm -rf /tmp/dj-beets-tmp"
rsync -av --delete --exclude '.git' ./ $(FILESERVER_HOST):/tmp/infra-nix/
@echo "Building and switching on fileserver..."
ssh -tt $(FILESERVER_HOST) "cd /tmp/infra-nix && sudo nixos-rebuild switch --flake '.#fileserver'"
@echo "✓ Fileserver deployed successfully"
@@ -118,28 +99,6 @@ install-fileserver:
echo "✓ Update hosts/fileserver/configuration.nix with the correct IP if needed"
@echo ""
install-k8s-server:
@echo "======================================"
@echo "Installing k8s-server..."
@echo "======================================"
@read -p "Enter k8s-server IP address: " IP; \
if [ -z "$$IP" ]; then \
echo "Error: IP address required"; \
exit 1; \
fi; \
echo "Copying install script to $$IP..."; \
scp install/install-k8s-server.sh root@$$IP:/tmp/; \
echo "Connecting to $$IP..."; \
ssh root@$$IP "bash /tmp/install-k8s-server.sh"; \
echo ""; \
echo "Fetching hardware config from $$IP..."; \
scp root@$$IP:/mnt/etc/nixos/hardware-configuration.nix hosts/k8s-server/hardware-configuration.nix; \
echo "✓ Hardware config saved to hosts/k8s-server/hardware-configuration.nix"; \
scp root@$$IP:/mnt/etc/nixos/configuration.nix hosts/k8s-server/configuration.installer.nix; \
echo "✓ Installer config saved to hosts/k8s-server/configuration.installer.nix"; \
echo "✓ Update hosts/k8s-server/configuration.nix with the correct IP if needed"
@echo ""
# Fetch hardware configuration targets
fetch-hwconfig-gateway:
@echo "======================================"
@@ -156,11 +115,3 @@ fetch-hwconfig-fileserver:
scp $(FILESERVER_HOST):/etc/nixos/hardware-configuration.nix hosts/fileserver/
@echo "✓ Hardware config saved to hosts/fileserver/hardware-configuration.nix"
@echo ""
fetch-hwconfig-k8s-server:
@echo "======================================"
@echo "Fetching hardware config from k8s-server..."
@echo "======================================"
scp $(K8S_HOST):/etc/nixos/hardware-configuration.nix hosts/k8s-server/
@echo "✓ Hardware config saved to hosts/k8s-server/hardware-configuration.nix"
@echo ""
-192
View File
@@ -1,4 +1,3 @@
````markdown
# infra-nix: NixOS Infrastructure
Dieses Repository enthält die deklarative NixOS-Infrastruktur für:
@@ -114,194 +113,3 @@ Access examples:
- rclone auth on headless: use `rclone authorize 'drive'` on a desktop and paste token
- Permissions: ensure `/data/daten` and `/data/daten/share` exist and are writable (`systemd-tmpfiles` creates them)
- Firewall: SMB ports 139/445 are open by module config
---
## K8s-Server: Docker Registry
The k8s-server runs a private Docker Registry v2 with self-signed TLS certificate. Authentication is handled by Caddy for external access.
**Access:**
- External URL: `https://registry.home.lindenfelser.de` (authenticated via Caddy)
- Internal URL: `https://10.202.82.7:5000` (direct, no auth - K8s pods)
- Default credentials: `admin` / `changeme` (Caddy basic auth)
- Storage: `/var/lib/docker-registry` (root partition)
### 1) Login from external machine
```bash
# Login to registry
docker login registry.home.lindenfelser.de
# Username: admin
# Password: changeme
```
### 2) Push an image
```bash
# Tag your image
docker tag myapp:latest registry.home.lindenfelser.de/myapp:latest
# Push to registry
docker push registry.home.lindenfelser.de/myapp:latest
```
### 3) Pull from Kubernetes pods
The k8s cluster is configured to authenticate automatically. Create a deployment:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 1
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: registry.home.lindenfelser.de/myapp:latest
```
### 4) Change registry password
Authentication is handled by Caddy. Generate new password hash and update:
```bash
# Generate new password hash locally
caddy hash-password --plaintext 'yournewpassword'
```
Then update the hash in [modules/gateway.nix](modules/gateway.nix) in the `basicauth` section and redeploy:
```bash
make deploy-gateway
```
### 5) Monitor storage usage
Check daily storage logs:
```bash
# View growth tracking
sudo journalctl -u docker-registry-growth-tracker
# View last 30 days of size tracking
sudo tail -n 30 /var/log/docker-registry-growth.log
# Check current usage
sudo du -sh /var/lib/docker-registry
df -h /
```
### 6) Manual garbage collection
Garbage collection runs automatically every Sunday at 03:00. To run manually:
```bash
sudo systemctl start docker-registry-garbage-collect
sudo journalctl -u docker-registry-garbage-collect -e
```
### 7) List images in registry
```bash
# List all repositories
curl -u admin:changeme https://registry.home.lindenfelser.de/v2/_catalog
# List tags for a specific image
curl -u admin:changeme https://registry.home.lindenfelser.de/v2/myapp/tags/list
```
### Troubleshooting
- **TLS certificate errors**: Registry uses self-signed certificate. External Docker clients need to add to insecure registries or install the cert
- **Authentication fails**: Verify Caddy basicauth configuration in [modules/gateway.nix](modules/gateway.nix). K8s pods access registry directly without auth.
- **Storage full**: Check root partition usage with `df -h /` and run garbage collection
- **K8s pods can't pull**: Verify `registries.yaml` points to internal registry (10.202.82.7:5000) and restart k3s: `sudo systemctl restart k3s`
````
---
## rclone (Google Drive)
The system has `rclone` installed and a systemd job to sync `/data` to a remote named `gdrive`:
- Service: `rclone-backup.service` (oneshot)
- Timer: `rclone-backup.timer` (runs daily 03:00)
- Log: `/var/log/rclone-backup.log`
### 1) Create the remote `gdrive`
Run on the fileserver:
```bash
sudo -i
rclone config
```
Then:
- n) New remote
- name: `gdrive`
- storage: `drive` (Google Drive)
- Use auto config? For headless server choose "No"
- Follow the printed instructions using another machine, or run on a desktop and copy the token
- Keep defaults unless you need a service account
- y) Yes to save
Verify:
```bash
rclone lsd gdrive:
rclone mkdir gdrive:backup-daten
```
### 2) Test backup manually
```bash
sudo systemctl start rclone-backup.service
sudo journalctl -u rclone-backup -e
sudo tail -n 100 /var/log/rclone-backup.log
```
### 3) Check/enable timer
```bash
systemctl list-timers '*rclone*'
sudo systemctl enable --now rclone-backup.timer
```
---
## SMB password for `danlin`
The share configuration:
- Protected share: `daten` → requires user `danlin`
- Guest share: `daten-share` → guest access allowed
Set the Samba password for `danlin` (independent from system login password):
```bash
sudo smbpasswd -a danlin
```
Useful commands:
```bash
sudo pdbedit -L # list Samba users
sudo systemctl status samba
sudo journalctl -u samba -e
```
Access examples:
- Windows: `\\fileserver\daten` or `\\fileserver\daten-share`
- macOS Finder: Go → Connect to Server → `smb://fileserver/daten` or `smb://fileserver/daten-share`
---
## Troubleshooting
- rclone auth on headless: use `rclone authorize 'drive'` on a desktop and paste token
- Permissions: ensure `/data/daten` and `/data/daten/share` exist and are writable (`systemd-tmpfiles` creates them)
- Firewall: SMB ports 139/445 are open by module config
-7
View File
@@ -24,13 +24,6 @@
./hosts/gateway/configuration.nix
];
};
k8s-server = lib.nixosSystem {
inherit system;
modules = [
./hosts/k8s-server/configuration.nix
];
};
};
};
}
@@ -1,49 +0,0 @@
{ config, pkgs, ... }:
{
imports = [ ./hardware-configuration.nix ];
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
security.sudo.wheelNeedsPassword = false;
networking.hostName = "k8s-server";
networking.useDHCP = false;
networking.interfaces.ens18.ipv4.addresses = [
{
address = "10.202.82.7";
prefixLength = 24;
}
];
networking.defaultGateway = "10.202.82.1";
networking.nameservers = [ "10.202.82.3" "10.202.82.4" ];
time.timeZone = "Europe/Berlin";
services.openssh.enable = true;
networking.firewall = {
enable = true;
allowedTCPPorts = [ 22 6443 ];
allowedUDPPorts = [ 8472 ];
};
users.users.danlin = {
isNormalUser = true;
extraGroups = [ "wheel" ];
initialPassword = "changeme";
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAzAW0DTpdQJaQOWDC3YJCmPc/veBQ0R3e1q9nOlWgxC danlin@MacBook-Pro-von-Daniel.fritz.box"
];
};
users.users.root.initialPassword = "root";
environment.systemPackages = with pkgs; [
vim
htop
];
system.stateVersion = "25.05";
}
-24
View File
@@ -1,24 +0,0 @@
{ config, pkgs, ... }:
{
imports = [
./hardware-configuration.nix
../../modules/common.nix
../../modules/kubernetes.nix
../../modules/docker-registry.nix
];
networking.hostName = "k8s-server";
networking.useDHCP = false;
networking.interfaces.ens18.ipv4.addresses = [
{
address = "10.202.82.7";
prefixLength = 24;
}
];
networking.defaultGateway = "10.202.82.1";
networking.nameservers = [ "10.202.82.3" "10.202.82.4" ];
system.stateVersion = "25.05";
}
@@ -1,37 +0,0 @@
# Do not modify this file! It was generated by nixos-generate-config
# and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead.
{ config, lib, pkgs, modulesPath, ... }:
{
imports =
[ (modulesPath + "/profiles/qemu-guest.nix")
];
boot.initrd.availableKernelModules = [ "uhci_hcd" "ehci_pci" "ahci" "virtio_pci" "virtio_scsi" "sd_mod" "sr_mod" ];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ];
fileSystems."/" =
{ device = "/dev/disk/by-uuid/839f8ac5-3391-4bc2-bf64-9df42ae6613b";
fsType = "ext4";
};
fileSystems."/boot" =
{ device = "/dev/disk/by-uuid/D25C-46DC";
fsType = "vfat";
options = [ "fmask=0022" "dmask=0022" ];
};
swapDevices = [ ];
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
# (the default) this is the recommended approach. When using systemd-networkd it's
# still possible to use this option, but it's recommended to use it in conjunction
# with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
networking.useDHCP = lib.mkDefault true;
# networking.interfaces.ens18.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
}
-92
View File
@@ -1,92 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
### CONFIG ###
OS_DISK=/dev/sda
HOSTNAME=k8s-server
STATE_VERSION="25.05"
TIMEZONE="Europe/Berlin"
USERNAME="danlin"
PASSWORD="changeme"
ROOT_PASS="root"
echo ">>> WARNUNG: ALLE DATEN auf ${OS_DISK} werden GELÖSCHT!"
echo ">>> Warte 5 Sekunden... (STRG+C zum Abbrechen)"
sleep 5
echo ">>> Partitioniere OS-Disk (${OS_DISK}) für EFI + ROOT..."
parted "${OS_DISK}" --script mklabel gpt
parted "${OS_DISK}" --script mkpart ESP fat32 1MiB 513MiB
parted "${OS_DISK}" --script set 1 esp on
parted "${OS_DISK}" --script mkpart primary ext4 513MiB 100%
echo ">>> Formatiere OS-Partitionen..."
mkfs.fat -F32 "${OS_DISK}1"
mkfs.ext4 -F "${OS_DISK}2"
echo ">>> Mounten..."
mount "${OS_DISK}2" /mnt
mkdir -p /mnt/boot
mount "${OS_DISK}1" /mnt/boot
echo ">>> Generiere NixOS-Config..."
nixos-generate-config --root /mnt
CONFIG=/mnt/etc/nixos/configuration.nix
cat > "${CONFIG}" <<EOF
{ config, pkgs, ... }:
{
imports = [ ./hardware-configuration.nix ];
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
security.sudo.wheelNeedsPassword = false;
networking.hostName = "${HOSTNAME}";
networking.useDHCP = false;
networking.interfaces.ens18.ipv4.addresses = [
{
address = "10.202.82.7";
prefixLength = 24;
}
];
networking.defaultGateway = "10.202.82.1";
networking.nameservers = [ "10.202.82.3" "10.202.82.4" ];
time.timeZone = "${TIMEZONE}";
services.openssh.enable = true;
networking.firewall = {
enable = true;
allowedTCPPorts = [ 22 6443 ];
allowedUDPPorts = [ 8472 ];
};
users.users.${USERNAME} = {
isNormalUser = true;
extraGroups = [ "wheel" ];
initialPassword = "${PASSWORD}";
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAzAW0DTpdQJaQOWDC3YJCmPc/veBQ0R3e1q9nOlWgxC danlin@MacBook-Pro-von-Daniel.fritz.box"
];
};
users.users.root.initialPassword = "${ROOT_PASS}";
environment.systemPackages = with pkgs; [
vim
htop
];
system.stateVersion = "${STATE_VERSION}";
}
EOF
echo ">>> Starte nixos-install..."
nixos-install
echo ">>> Fertig. Jetzt reboot ausführen."
+18 -3
View File
@@ -45,9 +45,24 @@
# QEMU guest agent (for VM integration when running as guest)
services.qemuGuest.enable = true;
# Automatic updates (security)
# Automatic updates — pull the committed config from Gitea and rebuild.
# Jeder Host baut sein eigenes Flake-Attribut (= networking.hostName).
system.autoUpgrade = {
enable = false; # Manual control via flake
allowReboot = false;
enable = true;
flake = "git+https://git.home.lindenfelser.de/danlin/infra-nix.git?ref=main#${config.networking.hostName}";
# nixpkgs beim Build auf die aktuelle Spitze von nixos-25.11 ziehen,
# statt den in main gepinnten flake.lock zu verwenden — so kommen
# wöchentlich frische Pakete ohne separaten Lock-Commit.
flags = [
"--override-input" "nixpkgs" "github:NixOS/nixpkgs/nixos-25.11"
];
dates = "Sun 04:00";
randomizedDelaySec = "30min";
# Kernel/Init-Updates greifen nur nach Reboot — nachts im Fenster erlauben.
allowReboot = true;
rebootWindow = {
lower = "04:00";
upper = "06:00";
};
};
}
-164
View File
@@ -1,164 +0,0 @@
{ config, pkgs, ... }:
{
########################################
# Docker Registry v2
########################################
services.dockerRegistry = {
enable = true;
port = 5000;
listenAddress = "0.0.0.0";
# Enable image deletion and garbage collection
enableDelete = true;
enableGarbageCollect = true;
garbageCollectDates = "Sun 03:00";
# Storage location (root partition)
storagePath = "/var/lib/docker-registry";
# TLS configuration (for internal access)
extraConfig = {
http = {
tls = {
certificate = "/var/lib/docker-registry/certs/registry.crt";
key = "/var/lib/docker-registry/certs/registry.key";
};
};
};
};
########################################
# Firewall
########################################
networking.firewall.allowedTCPPorts = [ 5000 ];
########################################
# Setup: directories, certificates, htpasswd
########################################
systemd.tmpfiles.rules = [
"d /var/lib/docker-registry 0755 root root -"
"d /var/lib/docker-registry/certs 0755 root root -"
];
# Generate self-signed certificate
systemd.services.docker-registry-setup = {
description = "Docker Registry initial setup";
wantedBy = [ "multi-user.target" ];
before = [ "docker-registry.service" ];
path = with pkgs; [ openssl apacheHttpd ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
# Create self-signed certificate if it doesn't exist
if [ ! -f /var/lib/docker-registry/certs/registry.crt ]; then
echo "Generating self-signed certificate for Docker Registry..."
${pkgs.openssl}/bin/openssl req -x509 -newkey rsa:4096 -nodes \
-keyout /var/lib/docker-registry/certs/registry.key \
-out /var/lib/docker-registry/certs/registry.crt \
-days 3650 \
-subj "/CN=registry.home.lindenfelser.de" \
-addext "subjectAltName=DNS:registry.home.lindenfelser.de,DNS:k8s-server,IP:10.202.82.7"
chmod 644 /var/lib/docker-registry/certs/registry.key
chmod 644 /var/lib/docker-registry/certs/registry.crt
echo "Certificate generated successfully"
fi
# Ensure correct ownership
chown -R docker-registry:docker-registry /var/lib/docker-registry
'';
};
########################################
# Storage Monitoring (80% threshold)
########################################
systemd.services.docker-registry-storage-check = {
description = "Check Docker Registry storage usage";
path = with pkgs; [ coreutils util-linux ];
serviceConfig = {
Type = "oneshot";
User = "root";
};
script = ''
REGISTRY_PATH="/var/lib/docker-registry"
THRESHOLD=80
if [ ! -d "$REGISTRY_PATH" ]; then
echo "Registry path does not exist yet"
exit 0
fi
# Get disk usage percentage of the filesystem containing the registry
USAGE=$(df -h "$REGISTRY_PATH" | awk 'NR==2 {print $5}' | sed 's/%//')
REGISTRY_SIZE=$(du -sh "$REGISTRY_PATH" | cut -f1)
echo "Docker Registry storage: $REGISTRY_SIZE (filesystem usage: $USAGE%)"
if [ "$USAGE" -gt "$THRESHOLD" ]; then
echo "WARNING: Filesystem usage ($USAGE%) exceeds threshold ($THRESHOLD%)"
echo "Consider cleaning up old images or expanding storage"
fi
'';
};
systemd.timers.docker-registry-storage-check = {
description = "Timer for Docker Registry storage check";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "daily";
Persistent = true;
};
};
########################################
# Daily Storage Growth Tracking
########################################
systemd.services.docker-registry-growth-tracker = {
description = "Track Docker Registry storage growth";
path = with pkgs; [ coreutils ];
serviceConfig = {
Type = "oneshot";
User = "root";
};
script = ''
REGISTRY_PATH="/var/lib/docker-registry"
LOG_FILE="/var/log/docker-registry-growth.log"
if [ ! -d "$REGISTRY_PATH" ]; then
echo "Registry path does not exist yet"
exit 0
fi
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
SIZE_BYTES=$(du -sb "$REGISTRY_PATH" | cut -f1)
SIZE_HUMAN=$(du -sh "$REGISTRY_PATH" | cut -f1)
echo "$TIMESTAMP | Size: $SIZE_HUMAN ($SIZE_BYTES bytes)" >> "$LOG_FILE"
# Keep only last 90 days of logs
if [ -f "$LOG_FILE" ]; then
tail -n 90 "$LOG_FILE" > "$LOG_FILE.tmp"
mv "$LOG_FILE.tmp" "$LOG_FILE"
fi
'';
};
systemd.timers.docker-registry-growth-tracker = {
description = "Timer for Docker Registry growth tracking";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "daily";
Persistent = true;
};
};
}
+10 -116
View File
@@ -147,11 +147,11 @@
"d /data/backup 0775 root root -"
"d /data/backup/timemachine 0775 danlin users -"
# FileBrowser config
"d /data/filebrowser 0755 root root -"
# Secrets
"d /data/secrets 0700 root root -"
# dj-beets project
"d /opt/dj-beets 0755 root root -"
];
########################################
@@ -165,15 +165,19 @@
image = "gtstef/filebrowser:stable";
autoStart = true;
ports = [ "8080:80" ];
volumes = [ "/data:/srv" ];
volumes = [
"/data:/srv"
"/data/filebrowser:/config"
];
environment = {
TZ = "Europe/Berlin";
FB_DATABASE = "/config/filebrowser.db";
};
environmentFiles = [ "/data/secrets/filebrowser.env" ];
};
virtualisation.oci-containers.containers.gitea = {
image = "gitea/gitea:1.25";
image = "gitea/gitea:latest";
autoStart = true;
ports = [ "3000:3000" "2222:2222" ];
volumes = [
@@ -192,35 +196,6 @@
};
};
virtualisation.oci-containers.containers.dj-beets-cli = {
image = "dj-beets:latest";
autoStart = false; # Run manually or via systemd service
volumes = [
"/data/daten/DJing:/data/daten/DJing"
"/opt/dj-beets/config.yaml:/etc/beets/config.yaml:ro"
"/opt/dj-beets/beatport_token.json:/opt/dj-beets/beatport_token.json"
];
environment = {
BEETS_CONFIG = "/etc/beets/config.yaml";
};
cmd = [ "tail" "-f" "/dev/null" ]; # Keep container alive
};
virtualisation.oci-containers.containers.dj-beets-web = {
image = "dj-beets:latest";
autoStart = true;
ports = [ "8337:8337" ];
volumes = [
"/data/daten/DJing:/data/daten/DJing"
"/opt/dj-beets/config.yaml:/etc/beets/config.yaml:ro"
"/opt/dj-beets/beatport_token.json:/opt/dj-beets/beatport_token.json"
];
environment = {
BEETS_CONFIG = "/etc/beets/config.yaml";
};
cmd = [ "beet" "web" ];
};
########################################
# Backup Services
########################################
@@ -255,94 +230,13 @@
pkgs.tmux
pkgs.htop
pkgs.podman-compose
# Beets CLI wrapper
(pkgs.writeShellScriptBin "beet" ''
exec ${pkgs.podman}/bin/podman run --rm -it \
-v /data/daten/DJing:/data/daten/DJing \
-v /opt/dj-beets/config.yaml:/etc/beets/config.yaml:ro \
-v /opt/dj-beets/beatport_token.json:/opt/dj-beets/beatport_token.json \
-e BEETS_CONFIG=/etc/beets/config.yaml \
-u $(id -u):$(id -g) \
dj-beets:latest \
beet "$@"
'')
];
########################################
# DJ-Beets Services
########################################
# Systemd service to build dj-beets image on deploy
systemd.services.dj-beets-build = {
description = "Build dj-beets Docker image";
wantedBy = [ "multi-user.target" ];
before = [ "podman-dj-beets-web.service" ];
after = [ "podman.service" ];
wants = [ "podman.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = pkgs.writeShellScript "build-dj-beets" ''
set -euo pipefail
if [ ! -f /opt/dj-beets/Dockerfile ]; then
echo "ERROR: Dockerfile not found at /opt/dj-beets" >&2
echo "Deploy with 'make deploy-fileserver' first" >&2
exit 1
fi
cd /opt/dj-beets
${pkgs.podman}/bin/podman build -t dj-beets:latest .
echo "dj-beets image built successfully"
'';
User = "root";
};
};
# Auto-import service using Docker container
systemd.services.dj-beets-autoimport = {
description = "Auto-import music into beets library via Docker";
after = [ "dj-beets-build.service" ];
wants = [ "dj-beets-build.service" ];
serviceConfig = {
Type = "oneshot";
ExecStart = pkgs.writeShellScript "beets-autoimport" ''
set -euo pipefail
INBOX="/data/daten/DJing/Inbox"
LOG="/data/daten/DJing/Data/BeetsAutoImport.log"
# Check if inbox has files
if ! find "$INBOX" -mindepth 1 -maxdepth 1 -type f -print -quit | grep -q .; then
echo "[$(date)] No files in Inbox, skipping" >> "$LOG"
exit 0
fi
echo "[$(date)] Starting beets import" >> "$LOG"
${pkgs.podman}/bin/podman run --rm \
-v /data/daten/DJing:/data/daten/DJing \
-v /opt/dj-beets/config.yaml:/etc/beets/config.yaml:ro \
-v /opt/dj-beets/beatport_token.json:/opt/dj-beets/beatport_token.json \
-e BEETS_CONFIG=/etc/beets/config.yaml \
localhost/dj-beets:latest \
beet import -q /data/daten/DJing/Inbox >> "$LOG" 2>&1
'';
# Use root so the service can access the root Podman image storage
User = "root";
Group = "root";
};
};
systemd.timers.dj-beets-autoimport = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "*:0/10"; # Every 10 minutes
Persistent = true;
};
};
########################################
# Firewall
########################################
networking.firewall.enable = true;
networking.firewall.allowedTCPPorts = [ 22 139 445 8080 8337 5357 3000 2222 ]; # 5357 WSD
networking.firewall.allowedTCPPorts = [ 22 139 445 8080 5357 3000 2222 ]; # 5357 WSD
networking.firewall.allowedUDPPorts = [ 137 138 3702 5353 ]; # NetBIOS + WSD (3702) + mDNS (5353)
}
+8 -16
View File
@@ -42,17 +42,16 @@
reverse_proxy 10.202.82.6:3000
'';
};
"registry.home.lindenfelser.de" = {
# Music-Server — NUR aus dem LAN 10.202.82.0/24 erreichbar.
# Alle anderen IPs bekommen 403, ohne dass der Upstream je angerufen wird.
"music.home.lindenfelser.de" = {
extraConfig = ''
@registry host registry.home.lindenfelser.de
basicauth @registry {
admin $2a$14$Ga5BCiHvtlfRjdnlI9bhseFnNZ8dwXsLz4t1FdSemA1mAUV/vA1oi
@lan client_ip 10.202.82.0/24
handle @lan {
reverse_proxy 10.202.82.195:80
}
reverse_proxy @registry https://10.202.82.7:5000 {
transport http {
tls
tls_insecure_skip_verify
}
handle {
respond "forbidden music ist nur aus dem LAN 10.202.82.0/24 erreichbar" 403
}
'';
};
@@ -69,7 +68,6 @@
80 # HTTP (Caddy)
443 # HTTPS (Caddy)
2222 # External SSH to Gitea
6443 # Kubernetes API to k8s-server
];
networking.firewall.allowedUDPPorts = [
@@ -85,7 +83,6 @@
# Hairpin NAT for LAN clients hitting gateway:2222 so replies go back via gateway
extraCommands = ''
iptables -t nat -A POSTROUTING -p tcp -d 10.202.82.6 --dport 2222 -j MASQUERADE
iptables -t nat -A POSTROUTING -p tcp -d 10.202.82.7 --dport 6443 -j MASQUERADE
'';
forwardPorts = [
{
@@ -93,11 +90,6 @@
sourcePort = 2222;
destination = "10.202.82.6:2222";
}
{
proto = "tcp";
sourcePort = 6443;
destination = "10.202.82.7:6443";
}
];
};
}
-49
View File
@@ -1,49 +0,0 @@
{ config, pkgs, ... }:
{
########################################
# k3s Single-Node (Traefik disabled)
########################################
services.k3s = {
enable = true;
role = "server";
# Initialize single-server cluster and disable bundled Traefik ingress.
extraFlags = toString [
"--cluster-init"
"--disable=traefik"
"--flannel-backend=vxlan"
"--tls-san=k8s.home.lindenfelser.de"
];
};
########################################
# Firewall for k3s
########################################
networking.firewall.enable = true;
networking.firewall.allowedTCPPorts = [
6443 # Kubernetes API
10250 # Kubelet metrics
5000 # Docker Registry
];
networking.firewall.allowedUDPPorts = [
8472 # flannel VXLAN
];
########################################
# Containerd registry configuration
########################################
environment.etc."rancher/k3s/registries.yaml" = {
text = ''
mirrors:
registry.home.lindenfelser.de:
endpoint:
- "https://10.202.82.7:5000"
configs:
"10.202.82.7:5000":
tls:
insecure_skip_verify: true
'';
mode = "0644";
};
}
-10
View File
@@ -1,10 +0,0 @@
.git
.venv
__pycache__
*.pyc
*.pyo
*.pyd
.Python
poetry.lock
data/
.DS_Store
-22
View File
@@ -1,22 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Current File",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/main.py",
"args": [
"--import",
"/Volumes/daten/DJing/Inbox",
"--config",
"${workspaceFolder}/config.yaml"
],
"console": "integratedTerminal"
}
]
}
-39
View File
@@ -1,39 +0,0 @@
FROM python:3.12-slim
# Install system dependencies for beets plugins
RUN apt-get update && apt-get install -y \
# For chromaprint/acoustid fingerprinting
libchromaprint-tools \
# For media file handling
ffmpeg \
# Build tools for some Python packages
gcc \
g++ \
&& rm -rf /var/lib/apt/lists/*
# Create app directory
WORKDIR /app
# Copy dependency files
COPY pyproject.toml ./
# Install Poetry
RUN pip install --no-cache-dir poetry
# Configure Poetry to not create virtual env (we're in container)
RUN poetry config virtualenvs.create false
# Install dependencies
RUN poetry install --no-root --no-interaction --no-ansi
# Copy config
COPY config.yaml /etc/beets/config.yaml
# Create data directories
RUN mkdir -p /data/Music /data/Inbox
# Set environment
ENV BEETS_CONFIG=/etc/beets/config.yaml
# Default command
CMD ["beet", "--help"]
-128
View File
@@ -1,128 +0,0 @@
# DJ Beets Setup
Diese Poetry-Umgebung ist dein eigenes Projekt als Ersatz für beets.
Hier verwaltest du Versionen und baust eigene Tools ein.
## Verwendung
### Lokal testen
```bash
cd src/dj-beets
poetry install
poetry run beet --version
```
### Deployment auf fileserver
Das komplette `src/dj-beets/` Projekt wird beim Deploy nach `/opt/dj-beets` kopiert:
```bash
make deploy-fileserver
```
Dies:
1. Kopiert dein `src/dj-beets/` nach `/opt/dj-beets` auf dem Server
2. Installiert alle Dependencies via Poetry (beets, beatport4, beetcamp, etc.)
3. Nutzt `poetry run beet` für alle beets-Operationen
**Wichtig**: Dein lokales Projekt ist die einzige Quelle. Kein externes GitHub-Repo mehr!
## Development Workflow
1. **Änderungen lokal testen**:
```bash
cd src/dj-beets
poetry install
poetry run beet --version
# Deine Änderungen testen
```
2. **Änderungen committen & deployen**:
```bash
git add src/dj-beets/
git commit -m "Update dj-beets project"
git push
# Auf fileserver deployen
make deploy-fileserver
```
3. **Auf fileserver nutzen**:
```bash
ssh fileserver
cd /opt/dj-beets
poetry run beet --version
# Auto-Import läuft automatisch alle 10 Minuten via Timer
```
## Plugins
Alle Plugins werden via `pyproject.toml` verwaltet:
- **beets** ^2.3.1: Basis-Library-Manager
- **beets-beatport4** ^0.4.1: Beatport API v4 Integration
- **beetcamp** ^0.9.3: Bandcamp Autotagger
- **requests, beautifulsoup4, html5lib**: Web-Scraping Dependencies
Weitere Plugins kannst du direkt in `pyproject.toml` hinzufügen.
## Konfiguration
Die beets-Config liegt in `config.yaml` (wird nach `/opt/dj-beets/config.yaml` deployed).
### Beatport Authentication
Beatport4 nutzt Token-basierte Authentifizierung. Token wird in `/data/daten/DJing/Data/beatport_token.json` gespeichert.
**Einmalig authentifizieren:**
```bash
# Auf fileserver
ssh danlin@10.202.82.6
beet beatport4 auth
# Folge den Prompts für Beatport Login
# Oder lokal
cd src/dj-beets
poetry run beet beatport4 auth
```
Das Token-File wird automatisch in allen Containern gemountet und bleibt nach Rebuilds erhalten.
### API Tokens
- **Discogs**: Token in `config.yaml` → `discogs.user_token`
- **Beatport**: Token-File → `/data/daten/DJing/Data/beatport_token.json`
Wichtige Pfade:
- **Library**: `/data/daten/DJing/Data/MusicLibrary.db`
- **Music**: `/data/daten/DJing/Music`
- **Inbox**: `/data/daten/DJing/Inbox`
- **Project**: `/opt/dj-beets`
## Updates
### Eigenes Projekt updaten:
```bash
# Lokal ändern
cd src/dj-beets
# z.B. neue Dependency hinzufügen
poetry add some-new-package
# Deployen
make deploy-fileserver
```
### Dependency-Updates:
```bash
cd src/dj-beets
poetry update
make deploy-fileserver
```
## Auto-Import
Der Auto-Import läuft alle 10 Minuten via systemd Timer und nutzt:
```bash
cd /opt/dj-beets
poetry run beet import -q /data/daten/DJing/Inbox
```
-7
View File
@@ -1,7 +0,0 @@
{
"access_token": "mknr9cGYMcVD85uG8m0av2pCXaihUJ",
"expires_in": 36000,
"token_type": "Bearer",
"scope": "app:docs user:dj",
"refresh_token": "CsMC8Ya5EzLup27Y8DGDX8LwkFIvVF"
}
-139
View File
@@ -1,139 +0,0 @@
# Beets config for fileserver
# Paths point to /data/daten/DJing structure
# Where imported music will be organized into
directory: /data/daten/DJing/Music
# Beets SQLite library database
library: /data/daten/DJing/Data/MusicLibrary.db
# State + import log
statefile: /data/daten/DJing/Data/state.pickle
import:
write: yes # write tags to files
move: yes # move files into directory
copy: no
timid: no # don't ask for confirmation on every match
log: /data/daten/DJing/Data/BeetsImport.log
incremental: yes
autotag: yes
resume: no # don't ask to resume
quiet: yes # less output
quiet_fallback: asis # skip if no good match
none_rec_action: skip # skip if no recommendation
duplicate_action: skip # skip duplicates automatically (was: ask)
group_albums: yes
ui:
color: yes
# Friendly filesystem names
asciify_paths: yes
per_disc_numbering: yes
# Replace problematic characters in paths
replace:
"[\\/]": _
":": _
"\\?": _
'"': _
"\\*": _
"<": _
">": _
"\\|": _
# Plugins
plugins:
- fetchart
# - mbsync
# - discogs
- duplicates
- scrub
- convert
- info
- fromfilename
- ftintitle
- web
- zero
- chroma
- beatport4
- bandcamp
- inline
# Cover Art
fetchart:
auto: yes
minwidth: 600
sources: filesystem coverart itunes amazon albumart
# MusicBrainz
# mbsync:
# album_query: ""
# artist_query: ""
# Discogs (token required)
# discogs:
# data_source_mismatch_penalty: 0.5
# user_token: "yZwmWYwexyfIdEYHxkWEEKiFxLIxoTHSKozBHdpp"
# Beatport (beets-beatport4)
beatport4:
data_source_mismatch_penalty: 0.3
art: no
tokenfile: /opt/dj-beets/beatport_token.json
# Bandcamp (beetcamp)
bandcamp:
data_source_mismatch_penalty: 0.2
preferred_media: Digital
search_max: 5
art: no
include_digital_only_tracks: true
# Cleanup noisy tags
scrub:
auto: yes
zero:
fields: comments lyrics
update_database: yes
# Duplicate detection keys
duplicates:
format: "$path"
keys:
- mb_trackid
- isrc
- acoustid_fingerprint
# Acoustic fingerprinting
chroma:
auto: yes
overwrite: no
# Paths with year/month prefix for better chronology
paths:
default: >-
%time{$added,%Y/%m}/%if{$albumartist,$albumartist,$artist}/$year - $album/
$artist - $title %if{$bpm,($bpm BPM%if{$initial_key,, $initial_key})}
singleton: >-
%time{$added,%Y/%m}/Singles/$artist/
$artist - $title %if{$bpm,($bpm BPM%if{$initial_key,, $initial_key})}
comp: >-
%time{$added,%Y/%m}/Compilations/$album ($year)/
$artist - $title %if{$bpm,($bpm BPM%if{$initial_key,, $initial_key})}
# Optional conversion recipe (requires ffmpeg in PATH)
convert:
auto: no
copy_album_art: yes
formats:
mp3:
command: ffmpeg -i $source -codec:a libmp3lame -qscale:a 2 $dest
extension: mp3
# Web UI
web:
host: 0.0.0.0 # Listen on all interfaces for Docker/network access
port: 8337
-28
View File
@@ -1,28 +0,0 @@
services:
beets:
build: .
container_name: dj-beets
volumes:
# Mount local data directory
- ./data:/data
# Mount config (optional override)
- ./config.yaml:/etc/beets/config.yaml:ro
environment:
- BEETS_CONFIG=/etc/beets/config.yaml
# Keep container running for interactive commands
stdin_open: true
tty: true
# Override default command to keep alive
command: tail -f /dev/null
beets-web:
build: .
container_name: dj-beets-web
volumes:
- ./data:/data
- ./config.yaml:/etc/beets/config.yaml:ro
environment:
- BEETS_CONFIG=/etc/beets/config.yaml
ports:
- "8337:8337"
command: beet web
-1425
View File
File diff suppressed because it is too large Load Diff
-25
View File
@@ -1,25 +0,0 @@
[tool.poetry]
name = "dj-beets"
version = "0.1.0"
description = "Beets + DJ-Plugins (Beatport/Bandcamp) für meine Library"
authors = ["Daniel <daniel@lindenfelser.de>"]
package-mode = false
[tool.poetry.dependencies]
# Python 3.10+ (NixOS 25.05)
python = ">=3.10,<3.14"
beets = "^2.3.1"
beets-beatport4 = "^0.4.1"
# Plugin dependencies
python3-discogs-client = "^2.3.15"
flask = "^3.1.0"
pyacoustid = "^1.3.0"
beetcamp = "^0.23.0"
[tool.poetry.extras]
beets = ["chroma", "discogs"]
[tool.poetry.scripts]
beet = "beets.ui:main"