Compare commits
23 Commits
42bb1c8092
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d2cfc1be5 | |||
| 35c02d6920 | |||
| a3d7803819 | |||
| 2fb046a880 | |||
| 70febee487 | |||
| 82ff0dc817 | |||
| 0f2ec2db3b | |||
| b782b4b8aa | |||
| d875c9c0cc | |||
| df498a8494 | |||
| 5803215702 | |||
| 2e78934b3a | |||
| ac80d261b8 | |||
| 7f1767345a | |||
| ad1dd816f4 | |||
| a0658b3a97 | |||
| cb0c3c791b | |||
| 7568578f60 | |||
| bb30e1ab3c | |||
| 96c16b6f52 | |||
| c8ccc76173 | |||
| 5f1b06da70 | |||
| d9a1dde5a5 |
@@ -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 ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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
@@ -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";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
+136
-111
@@ -147,11 +147,25 @@
|
||||
"d /data/backup 0775 root root -"
|
||||
"d /data/backup/timemachine 0775 danlin users -"
|
||||
|
||||
# FileBrowser config — muss dem Container-User (uid 1000) gehören,
|
||||
# das Image läuft non-root als filebrowser:1000.
|
||||
"d /data/filebrowser 0755 danlin users -"
|
||||
|
||||
# Das Image bringt eine config.yaml mit, die aber vom Bind-Mount auf
|
||||
# /home/filebrowser/data verdeckt wird — ohne sie startet es nicht.
|
||||
# "C" legt sie nur an, wenn sie fehlt; Änderungen bleiben erhalten.
|
||||
"C /data/filebrowser/config.yaml 0644 danlin users - ${pkgs.writeText "filebrowser-config.yaml" ''
|
||||
server:
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
logging:
|
||||
- levels: "info|warning|error"
|
||||
sources:
|
||||
- path: "/srv"
|
||||
''}"
|
||||
|
||||
# Secrets
|
||||
"d /data/secrets 0700 root root -"
|
||||
|
||||
# dj-beets project
|
||||
"d /opt/dj-beets 0755 root root -"
|
||||
];
|
||||
|
||||
########################################
|
||||
@@ -165,15 +179,23 @@
|
||||
image = "gtstef/filebrowser:stable";
|
||||
autoStart = true;
|
||||
ports = [ "8080:80" ];
|
||||
volumes = [ "/data:/srv" ];
|
||||
volumes = [
|
||||
"/data:/srv"
|
||||
# Das Image legt DB + config.yaml unter /home/filebrowser/data ab
|
||||
# (FILEBROWSER_DATABASE/-CONFIG). Ohne diesen Mount lebt die Datenbank
|
||||
# nur im Container und ist nach jedem Neustart weg.
|
||||
"/data/filebrowser:/home/filebrowser/data"
|
||||
];
|
||||
environment = {
|
||||
TZ = "Europe/Berlin";
|
||||
};
|
||||
environmentFiles = [ "/data/secrets/filebrowser.env" ];
|
||||
# Läuft als non-root (uid 1000) und dürfte Port 80 sonst nicht binden.
|
||||
extraOptions = [ "--sysctl" "net.ipv4.ip_unprivileged_port_start=0" ];
|
||||
};
|
||||
|
||||
virtualisation.oci-containers.containers.gitea = {
|
||||
image = "gitea/gitea:1.25";
|
||||
image = "gitea/gitea:latest";
|
||||
autoStart = true;
|
||||
ports = [ "3000:3000" "2222:2222" ];
|
||||
volumes = [
|
||||
@@ -189,36 +211,88 @@
|
||||
GITEA__server__SSH_PORT = "2222";
|
||||
GITEA__server__SSH_LISTEN_PORT = "2222";
|
||||
GITEA__server__START_SSH_SERVER = "true";
|
||||
# Actions (CI) aktivieren — Voraussetzung für den act_runner unten.
|
||||
GITEA__actions__ENABLED = "true";
|
||||
};
|
||||
};
|
||||
|
||||
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";
|
||||
########################################
|
||||
# Gitea Actions Runner
|
||||
########################################
|
||||
# Job-Container laufen über das bereits vorhandene Podman (das Modul erkennt
|
||||
# das automatisch: DOCKER_HOST -> /run/podman/podman.sock, Gruppe "podman").
|
||||
services.gitea-actions-runner = {
|
||||
package = pkgs.gitea-actions-runner;
|
||||
instances.fileserver = {
|
||||
enable = true;
|
||||
name = "fileserver";
|
||||
# Interne LAN-URL — vom Host UND aus den Docker-Job-Containern erreichbar
|
||||
# (localhost/127.0.0.1 würde im Job-Container auf den Container selbst zeigen).
|
||||
url = "http://10.202.82.6:3000";
|
||||
# Registration-Token: in Gitea unter Site Admin → Actions → Runners →
|
||||
# "Create new Runner" erzeugen und in diese Datei schreiben (0600 root).
|
||||
tokenFile = "/data/secrets/gitea-runner-token";
|
||||
labels = [
|
||||
"ubuntu-latest:docker://node:20-bookworm"
|
||||
"ubuntu-22.04:docker://node:20-bookworm"
|
||||
"alpine:docker://alpine:3.19"
|
||||
];
|
||||
};
|
||||
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";
|
||||
# Reaper für verwaiste Job-Container. Wird der Runner mitten in einem Job
|
||||
# gestoppt (Reboot, autoUpgrade, Gitea-Neustart), läuft dessen Container mit
|
||||
# "sleep 10800" bis zu 3h weiter und wird nie abgeräumt. Dieser Timer entfernt
|
||||
# solche Container — aber NUR, wenn Gitea den zugehörigen Task als beendet
|
||||
# führt (Status 1/2/3/4). Laufende (6) oder unbekannte Tasks bleiben unberührt.
|
||||
systemd.services.gitea-runner-reaper = {
|
||||
description = "Reap orphaned Gitea Actions job containers";
|
||||
after = [ "podman.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = "root";
|
||||
ExecStart = pkgs.writeShellScript "gitea-runner-reaper" ''
|
||||
set -uo pipefail
|
||||
PODMAN=${pkgs.podman}/bin/podman
|
||||
|
||||
# Ohne laufenden Gitea-Container keine DB-Abfrage möglich -> nichts tun.
|
||||
if ! $PODMAN container exists gitea; then
|
||||
echo "gitea container not running — skipping reap"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
$PODMAN ps --filter name=GITEA-ACTIONS-TASK- --format '{{.Names}}' | while read -r c; do
|
||||
[ -n "$c" ] || continue
|
||||
tid=$(printf '%s' "$c" | sed -n 's/^GITEA-ACTIONS-TASK-\([0-9]\+\)_.*/\1/p')
|
||||
[ -n "$tid" ] || continue
|
||||
# immutable=1: lock-freier read-only Zugriff auf die (von Gitea offene) DB.
|
||||
st=$($PODMAN exec gitea sqlite3 "file:/data/gitea/gitea.db?immutable=1" \
|
||||
"SELECT status FROM action_task WHERE id=$tid;" 2>/dev/null)
|
||||
# Status: 1=success 2=failure 3=cancelled 4=skipped 5=waiting 6=running
|
||||
case "$st" in
|
||||
1|2|3|4)
|
||||
echo "reaping orphan $c (task $tid finished, status=$st)"
|
||||
$PODMAN rm -f "$c" >/dev/null 2>&1 || true
|
||||
;;
|
||||
*) : ;; # 6/5/leer/unbekannt -> in Ruhe lassen
|
||||
esac
|
||||
done
|
||||
|
||||
# Übrig gebliebene Actions-Netzwerke/Volumes und dangling Images aufräumen
|
||||
# (prune entfernt nur, was an KEINEN laufenden Container gebunden ist).
|
||||
$PODMAN network prune -f >/dev/null 2>&1 || true
|
||||
$PODMAN volume prune -f >/dev/null 2>&1 || true
|
||||
$PODMAN image prune -f >/dev/null 2>&1 || true
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
systemd.timers.gitea-runner-reaper = {
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnCalendar = "*:0/15";
|
||||
Persistent = true;
|
||||
};
|
||||
cmd = [ "beet" "web" ];
|
||||
};
|
||||
|
||||
########################################
|
||||
@@ -228,10 +302,42 @@
|
||||
description = "Backup /data to Google Drive via rclone";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = ''
|
||||
ExecStart = pkgs.writeShellScript "rclone-backup" ''
|
||||
set -uo pipefail
|
||||
|
||||
# Laufende SQLite-DBs lassen sich nicht als Datei kopieren — sie ändern
|
||||
# sich während des Transfers (md5-Fehler). Daher vorab ein konsistentes
|
||||
# Hot-Backup (.backup) in einen Snapshot-Ordner ziehen, der mitgesichert
|
||||
# wird; die Live-Dateien selbst schließen wir unten aus.
|
||||
SNAP=/data/db-snapshots
|
||||
mkdir -p "$SNAP"
|
||||
for db in \
|
||||
/data/gitea/gitea/gitea.db \
|
||||
/data/filebrowser/database.db \
|
||||
/data/daten/DJing/Data/MusicLibrary.db; do
|
||||
[ -f "$db" ] || continue
|
||||
out="$SNAP/$(echo "$db" | sed 's#^/data/##; s#/#_#g')"
|
||||
# Echtes SQLite -> konsistentes .backup. Andernfalls (filebrowser
|
||||
# nutzt z.B. BoltDB) oder bei Sperre auf eine Rohkopie zurückfallen.
|
||||
if ${pkgs.sqlite}/bin/sqlite3 "$db" ".timeout 15000" ".backup '$out'" 2>/dev/null && [ -s "$out" ]; then
|
||||
:
|
||||
else
|
||||
cp -f "$db" "$out" || echo "WARN: snapshot of $db failed" >&2
|
||||
fi
|
||||
done
|
||||
|
||||
${pkgs.rclone}/bin/rclone sync /data gdrive:backup-daten \
|
||||
--fast-list \
|
||||
--drive-stop-on-upload-limit \
|
||||
--tpslimit 25 \
|
||||
--drive-pacer-min-sleep 10ms \
|
||||
--retries 3 \
|
||||
--exclude "**/*.db-journal" \
|
||||
--exclude "**/*.db-wal" \
|
||||
--exclude "**/*.db-shm" \
|
||||
--exclude "gitea/gitea/gitea.db" \
|
||||
--exclude "filebrowser/database.db" \
|
||||
--exclude "daten/DJing/Data/MusicLibrary.db" \
|
||||
--log-file=/var/log/rclone-backup.log \
|
||||
--log-level=INFO
|
||||
'';
|
||||
@@ -255,94 +361,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
@@ -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";
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -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";
|
||||
};
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
.git
|
||||
.venv
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
poetry.lock
|
||||
data/
|
||||
.DS_Store
|
||||
Vendored
-22
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"access_token": "mknr9cGYMcVD85uG8m0av2pCXaihUJ",
|
||||
"expires_in": 36000,
|
||||
"token_type": "Bearer",
|
||||
"scope": "app:docs user:dj",
|
||||
"refresh_token": "CsMC8Ya5EzLup27Y8DGDX8LwkFIvVF"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
Generated
-1425
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
Reference in New Issue
Block a user