Compare commits

...

13 Commits

Author SHA1 Message Date
danlin 3d2cfc1be5 rclone-backup: fall back to raw copy for non-SQLite DBs
filebrowser's database.db is BoltDB, not SQLite, so sqlite .backup left a
0-byte snapshot. Try .backup and fall back to cp when it isn't SQLite (or
is locked), so every DB gets a usable snapshot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 09:42:38 +02:00
danlin 35c02d6920 rclone-backup: snapshot live SQLite DBs instead of copying them raw
Copying gitea.db / filebrowser database.db / MusicLibrary.db while they
are being written fails md5 verification every run (the file changes mid
-transfer). Take a consistent sqlite .backup into /data/db-snapshots and
exclude the live DB files from the sync, so the job completes cleanly and
the backup holds restorable DB copies.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 09:38:55 +02:00
danlin a3d7803819 rclone-backup: exclude live SQLite sidecar files
The nightly sync failed every night with 'corrupted on transfer: md5
hashes differ' on gitea.db-journal: rclone copies the volatile SQLite
journal while Gitea rewrites/removes it. That IO error also made rclone
skip the delete phase, so the backup drifted out of sync. Exclude the
transient *.db-journal/-wal/-shm files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 09:31:44 +02:00
danlin 2fb046a880 rclone-backup: raise throughput now that we have our own Drive quota
With the dedicated client_id the shared-project rate limits are gone, so
lift the pacing: tpslimit 10->25 and drive-pacer-min-sleep 100ms->10ms
(the old 100ms capped requests at ~10/s and would have throttled the
higher tpslimit anyway).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 19:34:20 +02:00
danlin 70febee487 Reap orphaned Gitea Actions job containers
When the runner is stopped mid-job (reboot, autoUpgrade, gitea restart),
its job container keeps running its `sleep 10800` entrypoint and is never
cleaned up, and unused images pile up. Add a 15-min timer that removes a
GITEA-ACTIONS-TASK-N container only when Gitea's DB reports that task as
finished (status 1/2/3/4); running/unknown tasks are left untouched. Also
prune unused actions networks/volumes and dangling images.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 17:35:26 +02:00
danlin 82ff0dc817 rclone-backup: throttle API calls to survive Drive rate limits
The nightly sync has failed since Jul 18 with RATE_LIMIT_EXCEEDED,
transferring 0 B. Limit transactions and pace Drive requests, and retry
a few times before giving up. This mitigates rather than fixes the cause
(the remote still uses rclone's shared default client_id).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 07:58:39 +02:00
danlin 0f2ec2db3b filebrowser: seed config.yaml that the bind mount would otherwise hide
The image ships /home/filebrowser/data/config.yaml, but mounting the host
directory there hides it and the app refuses to start. Seed it with
tmpfiles 'C' (create-if-missing) so it stays editable afterwards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 07:51:18 +02:00
danlin b782b4b8aa Fix filebrowser: persist database and allow port 80 bind
The image switched to the FILEBROWSER_* env scheme and now runs as
non-root (uid 1000), which broke two things:

- It binds :80 inside the container, which a non-root user may not do
  ("bind: permission denied" since the Jul 19 reboot). Allow it via
  net.ipv4.ip_unprivileged_port_start=0 instead of running as root.
- It stores DB/config under /home/filebrowser/data, not /config, so the
  old FB_DATABASE + /config mount were ignored and the database lived
  inside the container (lost on every restart). Mount the host dir there
  and give it to uid 1000.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 07:49:43 +02:00
danlin d875c9c0cc Remove temporary smoke-test workflow
Runner verified working end-to-end (Podman executor, alpine job, success).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 22:22:06 +02:00
danlin df498a8494 smoke-test: use sh shell (alpine has no bash)
smoke-test / smoke (push) Successful in 13s
2026-07-18 22:21:16 +02:00
danlin 5803215702 Add temporary smoke-test workflow (to be removed)
smoke-test / smoke (push) Failing after 13m30s
2026-07-18 21:53:50 +02:00
danlin 2e78934b3a Use Podman (not Docker) as the gitea-actions-runner backend
The default docker package is marked insecure; the runner module already
supports Podman natively (DOCKER_HOST -> /run/podman/podman.sock, group
podman), and the socket + group are already present on the host.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 21:37:09 +02:00
danlin ac80d261b8 Add Gitea Actions runner (Docker executor)
- Enable Gitea Actions in the container (GITEA__actions__ENABLED)
- Run dockerd alongside podman for job containers
- services.gitea-actions-runner: instance-wide runner "fileserver",
  Docker labels, registered against the internal LAN URL; reads the
  registration token from /data/secrets/gitea-runner-token

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 21:14:41 +02:00
+136 -5
View File
@@ -147,8 +147,22 @@
"d /data/backup 0775 root root -" "d /data/backup 0775 root root -"
"d /data/backup/timemachine 0775 danlin users -" "d /data/backup/timemachine 0775 danlin users -"
# FileBrowser config # FileBrowser config — muss dem Container-User (uid 1000) gehören,
"d /data/filebrowser 0755 root root -" # 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 # Secrets
"d /data/secrets 0700 root root -" "d /data/secrets 0700 root root -"
@@ -167,13 +181,17 @@
ports = [ "8080:80" ]; ports = [ "8080:80" ];
volumes = [ volumes = [
"/data:/srv" "/data:/srv"
"/data/filebrowser:/config" # 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 = { environment = {
TZ = "Europe/Berlin"; TZ = "Europe/Berlin";
FB_DATABASE = "/config/filebrowser.db";
}; };
environmentFiles = [ "/data/secrets/filebrowser.env" ]; 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 = { virtualisation.oci-containers.containers.gitea = {
@@ -193,6 +211,87 @@
GITEA__server__SSH_PORT = "2222"; GITEA__server__SSH_PORT = "2222";
GITEA__server__SSH_LISTEN_PORT = "2222"; GITEA__server__SSH_LISTEN_PORT = "2222";
GITEA__server__START_SSH_SERVER = "true"; GITEA__server__START_SSH_SERVER = "true";
# Actions (CI) aktivieren — Voraussetzung für den act_runner unten.
GITEA__actions__ENABLED = "true";
};
};
########################################
# 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"
];
};
};
# 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;
}; };
}; };
@@ -203,10 +302,42 @@
description = "Backup /data to Google Drive via rclone"; description = "Backup /data to Google Drive via rclone";
serviceConfig = { serviceConfig = {
Type = "oneshot"; 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 \ ${pkgs.rclone}/bin/rclone sync /data gdrive:backup-daten \
--fast-list \ --fast-list \
--drive-stop-on-upload-limit \ --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-file=/var/log/rclone-backup.log \
--log-level=INFO --log-level=INFO
''; '';