feat: systemd-Unit, Installationsskript, Dockerfile und compose.yml

Unit mit StateDirectory/RuntimeDirectory, ProtectSystem=full und
MountFlags=shared; compose mit SYS_ADMIN, DAC_READ_SEARCH,
apparmor:unconfined und rshared-Bind für /mnt/netmover (Propagation zum
Host im Container getestet).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-10-10 19:49:03 +02:00
co-authored by Claude Opus 5.5
parent 00a0537085
commit dd9177859e
6 changed files with 182 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
.venv
.git
data
**/__pycache__
.pytest_cache
.mypy_cache
.ruff_cache
tests
+29
View File
@@ -0,0 +1,29 @@
# NetMover im Container (Alternative zum systemd-Dienst, siehe README).
# Braucht zur Laufzeit CAP_SYS_ADMIN und ein Mount-Verzeichnis mit
# Propagation „rshared“ – sonst sieht der Host die Mounts nicht.
FROM python:3.12-slim-trixie AS build
WORKDIR /src
COPY pyproject.toml README.md ./
COPY src ./src
RUN pip wheel --no-cache-dir --wheel-dir /wheels .
FROM python:3.12-slim-trixie
RUN apt-get update \
&& apt-get install -y --no-install-recommends cifs-utils nfs-common \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /wheels /wheels
RUN pip install --no-cache-dir /wheels/*.whl && rm -rf /wheels
ENV NETMOVER_HOST=0.0.0.0 \
NETMOVER_PORT=8080 \
NETMOVER_DATA_DIR=/var/lib/netmover \
NETMOVER_RUN_DIR=/run/netmover \
NETMOVER_MOUNT_BASE=/mnt/netmover \
PYTHONUNBUFFERED=1
VOLUME ["/var/lib/netmover"]
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s \
CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8080/healthz', timeout=4).status == 200 else 1)"
CMD ["netmover", "serve"]
+37
View File
@@ -0,0 +1,37 @@
# NetMover per Docker Compose. Vorher auf dem Host:
# mkdir -p /mnt/netmover
# Die Weboberfläche ist nur auf dem Host selbst erreichbar (127.0.0.1:8080).
services:
netmover:
build: .
image: netmover:0.1.0
container_name: netmover
restart: unless-stopped
cap_add:
- SYS_ADMIN # mount/umount
- DAC_READ_SEARCH # mount.cifs liest die Credentials-Datei
security_opt:
- apparmor:unconfined # Dockers AppArmor-Profil verbietet mount
ports:
- "127.0.0.1:8080:8080"
environment:
NETMOVER_LOG_LEVEL: INFO
# Hinter einem Reverse Proxy mit HTTPS:
# NETMOVER_COOKIE_SECURE: "true"
# NETMOVER_FORWARDED_ALLOW_IPS: "172.16.0.0/12"
volumes:
- netmover-data:/var/lib/netmover
# WICHTIG: rshared – sonst bleiben die Mounts im Container gefangen und
# weder der Host noch andere Container sehen sie.
- type: bind
source: /mnt/netmover
target: /mnt/netmover
bind:
propagation: rshared
# Lokale Ziele (z. B. Paperless-Consume-Ordner) zusätzlich einbinden:
# - /opt/paperless/consume:/opt/paperless/consume
tmpfs:
- /run/netmover:mode=0700
volumes:
netmover-data:
+46
View File
@@ -0,0 +1,46 @@
#!/bin/sh
# NetMover auf Debian 13 installieren bzw. aktualisieren (als root ausführen).
#
# ./deploy/install.sh # aus diesem Verzeichnis installieren
# ./deploy/install.sh 'git+https://git.wilde-server.de/florian.wilde/netmover.git@v0.1.0'
set -eu
SOURCE="${1:-$(cd "$(dirname "$0")/.." && pwd)}"
PREFIX=/opt/netmover
if [ "$(id -u)" -ne 0 ]; then
echo "Bitte als root ausführen." >&2
exit 1
fi
echo "==> Pakete"
apt-get update -qq
apt-get install -y -qq python3 python3-venv cifs-utils nfs-common >/dev/null
echo "==> Virtuelle Umgebung in $PREFIX/venv"
install -d -m 0755 "$PREFIX"
[ -x "$PREFIX/venv/bin/python" ] || python3 -m venv "$PREFIX/venv"
"$PREFIX/venv/bin/pip" install -q --upgrade pip
"$PREFIX/venv/bin/pip" install -q --upgrade "$SOURCE"
echo "==> Konfiguration"
install -d -m 0755 /etc/netmover
if [ ! -f /etc/netmover/netmover.env ]; then
install -m 0640 "$(dirname "$0")/netmover.env" /etc/netmover/netmover.env
fi
install -d -m 0755 /mnt/netmover
echo "==> systemd"
install -m 0644 "$(dirname "$0")/netmover.service" /etc/systemd/system/netmover.service
systemctl daemon-reload
systemctl enable netmover >/dev/null 2>&1
systemctl restart netmover
sleep 2
systemctl --no-pager --lines=0 status netmover || true
echo
if journalctl -u netmover -n 200 --no-pager | grep -q "setup.required"; then
echo "Ersteinrichtung: Einrichtungscode aus dem Log:"
journalctl -u netmover -n 200 --no-pager | grep "setup.required" | tail -1
fi
echo "Fertig. Weboberfläche: http://127.0.0.1:8080 (README: SSH-Tunnel / Reverse Proxy)"
+17
View File
@@ -0,0 +1,17 @@
# Umgebungsvariablen für NetMover (/etc/netmover/netmover.env).
# Nach Änderungen: systemctl restart netmover
# Nur lokal erreichbar – für Zugriff aus dem LAN einen Reverse Proxy mit TLS
# davorsetzen (README, „Reverse Proxy“) oder per SSH-Tunnel arbeiten.
NETMOVER_HOST=127.0.0.1
NETMOVER_PORT=8080
# Hinter einem Reverse Proxy mit HTTPS:
# NETMOVER_COOKIE_SECURE=true
# NETMOVER_FORWARDED_ALLOW_IPS=127.0.0.1
# NETMOVER_MOUNT_BASE=/mnt/netmover
# NETMOVER_SESSION_HOURS=12
NETMOVER_LOG_LEVEL=INFO
# logfmt (gut lesbar im Journal) oder json
NETMOVER_LOG_FORMAT=logfmt
+45
View File
@@ -0,0 +1,45 @@
# NetMover als systemd-Dienst direkt auf dem Host (empfohlen).
# Installation: siehe README, Abschnitt „Installation“.
[Unit]
Description=NetMover – NFS/SMB-Mounts und zeitgesteuertes Verschieben
Documentation=https://git.wilde-server.de/florian.wilde/netmover
Wants=network-online.target
After=network-online.target remote-fs-pre.target
[Service]
Type=exec
# root ist nötig: mount/umount brauchen CAP_SYS_ADMIN, und Dateien auf beliebigen
# Freigaben sollen mit den richtigen Eigentümern geschrieben werden (siehe README).
User=root
EnvironmentFile=-/etc/netmover/netmover.env
ExecStart=/opt/netmover/venv/bin/netmover serve
Restart=on-failure
RestartSec=5
# Beim Stoppen werden Mounts bewusst NICHT ausgehängt.
KillMode=mixed
TimeoutStopSec=20
# /var/lib/netmover (Datenbank, Schlüssel) und /run/netmover (kurzlebige
# Credentials-Dateien für mount.cifs) – beide nur für root lesbar.
StateDirectory=netmover
StateDirectoryMode=0700
RuntimeDirectory=netmover
RuntimeDirectoryMode=0700
# Härtung, soweit mit Mounten vereinbar.
# Die Optionen unten erzeugen einen eigenen Mount-Namespace für den Dienst.
# MountFlags=shared sorgt dafür, dass Mounts unter /mnt/netmover trotzdem im
# System sichtbar sind (prüfen mit: findmnt -R /mnt/netmover).
MountFlags=shared
# /usr, /boot und /etc schreibgeschützt; /mnt, /opt, /srv, /var bleiben schreibbar
# (lokale Ziele wie /opt/paperless/consume funktionieren also).
ProtectSystem=full
PrivateTmp=yes
NoNewPrivileges=yes
ProtectControlGroups=yes
LockPersonality=yes
RestrictRealtime=yes
UMask=0022
[Install]
WantedBy=multi-user.target