Run containers as the host user instead of root
Everything the containers wrote into the bind mounts (surreal_data, notebook_data) was owned by root, so the host user could not delete or back up his own podcast data — prune_podcast_data.py and update_stack.sh had to detour through `docker compose exec` for every rm and tar. That was not a requirement, just the default: the open_notebook image declares no USER, and the compose file even overrode surrealdb's own non-root user (65532) with `user: root`, under the comment "Required for bind mounts on Linux" — which is not true. Both services now run as user: "1000:1000". Two things this needs: - HOME=/tmp for open_notebook. Without it HOME resolves to "/" for a non-root uid, uv cannot create /.cache/uv, and api + worker exit 2 at startup. Verified by running the image as uid 1000 both ways. - The data directories must be owned by that uid. Existing data was adopted with a throwaway root container (chown -R), no sudo needed. Both scripts drop the container detour and operate on the host directly, which is simpler and now honest. smoke_test.sh gained two checks so a silent regression to root cannot go unnoticed: the container's uid must match the host user, and no foreign-owned files may exist under the data directories. Verified: containers run as uid 1000, new files land as dschlueter and are deletable without sudo, SurrealDB writes as 1000, a source can be created, embedded and deleted through the API, and the full smoke test is green. Note this deviates from what the image expects (it assumes root), so it is exactly the kind of assumption an update can break — hence the smoke-test checks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
09c6d910a7
commit
9e6d790830
6 changed files with 96 additions and 40 deletions
|
|
@ -22,7 +22,7 @@ from __future__ import annotations
|
|||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
|
|
@ -69,28 +69,25 @@ def to_host_path(audio_file: str) -> Path | None:
|
|||
return p if p.is_absolute() and p.exists() else None
|
||||
|
||||
|
||||
def to_container_path(host_path: Path) -> str:
|
||||
"""Pfad auf dem Host -> Container-Pfad (Umkehrung von to_host_path)."""
|
||||
rel = host_path.resolve().relative_to(HOST_PREFIX.resolve())
|
||||
return CONTAINER_PREFIX + str(rel)
|
||||
def delete_dirs(paths: list[Path]) -> None:
|
||||
"""Loescht die Verzeichnisse direkt auf dem Host.
|
||||
|
||||
|
||||
def delete_in_container(paths: list[Path]) -> None:
|
||||
"""Loescht im Container statt auf dem Host.
|
||||
|
||||
Der open_notebook-Container laeuft als root und legt die Episodenordner
|
||||
entsprechend root:root an — der Host-User darf sie nicht entfernen. Statt
|
||||
dafuer sudo zu verlangen, loeschen wir dort, wo die Rechte ohnehin stimmen.
|
||||
Das geht, weil die Container als Host-User laufen (user: "1000:1000" in
|
||||
docker-compose.yml) und die Episodenordner deshalb uns gehoeren. Liefen sie als
|
||||
root — der Default, wenn man den user-Eintrag entfernt — schlaegt das hier mit
|
||||
PermissionError fehl; dann gehoert der Eintrag zurueck in die Compose-Datei.
|
||||
"""
|
||||
targets = [to_container_path(p) for p in paths]
|
||||
guard = CONTAINER_PREFIX + "podcasts/episodes/"
|
||||
for t in targets: # Sicherheitsnetz gegen Pfade ausserhalb der Episoden.
|
||||
if not t.startswith(guard):
|
||||
die(f"Abbruch: {t} liegt ausserhalb von {guard}")
|
||||
subprocess.run(
|
||||
["docker", "compose", "exec", "-T", "open_notebook", "rm", "-rf", "--", *targets],
|
||||
cwd=REPO, check=True, capture_output=True,
|
||||
)
|
||||
guard = (EPISODES_DIR).resolve()
|
||||
for p in paths: # Sicherheitsnetz gegen Pfade ausserhalb der Episoden.
|
||||
if guard not in p.resolve().parents:
|
||||
die(f"Abbruch: {p} liegt ausserhalb von {guard}")
|
||||
for p in paths:
|
||||
try:
|
||||
shutil.rmtree(p)
|
||||
except PermissionError as e:
|
||||
die(f"Keine Berechtigung fuer {p} ({e}).\n"
|
||||
"Gehoeren die Daten root? Dann laeuft der Container als root — pruefe\n"
|
||||
"den Eintrag user: \"1000:1000\" in docker-compose.yml.")
|
||||
|
||||
|
||||
def dir_size_mb(path: Path) -> float:
|
||||
|
|
@ -185,13 +182,7 @@ def main() -> int:
|
|||
return 0
|
||||
|
||||
victims = [d for d, _ in orphans] + [c for c, _, _ in clip_dirs]
|
||||
try:
|
||||
delete_in_container(victims)
|
||||
except FileNotFoundError:
|
||||
die("docker nicht gefunden — das Loeschen laeuft im Container "
|
||||
"(die Ordner gehoeren root).")
|
||||
except subprocess.CalledProcessError as e:
|
||||
die(f"Loeschen im Container fehlgeschlagen: {e.stderr.decode().strip()}")
|
||||
delete_dirs(victims)
|
||||
|
||||
for d, _ in orphans:
|
||||
print(f"geloescht: {d.name}")
|
||||
|
|
|
|||
|
|
@ -41,6 +41,26 @@ else
|
|||
fi
|
||||
rm -f /tmp/.st_prov
|
||||
|
||||
# Laufen die Container als Host-User? Sonst gehoeren alle neu geschriebenen Daten
|
||||
# root, und Aufraeumen/Sichern auf dem Host scheitert an den Rechten (prune_podcast_data.py,
|
||||
# update_stack.sh verlassen sich darauf).
|
||||
uid="$(id -u)"
|
||||
if [ "$(inapp id -u | tr -d '\r')" = "$uid" ]; then
|
||||
ok "open_notebook laeuft als Host-User (uid $uid), nicht als root"
|
||||
else
|
||||
fail "open_notebook laeuft NICHT als uid $uid — neue Dateien gehoerten root.
|
||||
user: \"$uid:$(id -g)\" in docker-compose.yml pruefen."
|
||||
fi
|
||||
|
||||
foreign="$(find surreal_data notebook_data ! -user "$USER" 2>/dev/null | wc -l)"
|
||||
if [ "$foreign" -eq 0 ]; then
|
||||
ok "keine fremd-eigenen Dateien in surreal_data/ und notebook_data/"
|
||||
else
|
||||
fail "$foreign Datei(en) in surreal_data/notebook_data gehoeren nicht $USER —
|
||||
uebernehmen mit: docker run --rm -u 0 -v \"\$PWD:/work\" -w /work \\
|
||||
--entrypoint chown \$IMAGE -R $uid:$(id -g) surreal_data notebook_data"
|
||||
fi
|
||||
|
||||
sec "Lokale Sprachdienste (vom Container aus)"
|
||||
|
||||
if inapp "$PY" -c "
|
||||
|
|
|
|||
|
|
@ -53,19 +53,17 @@ mkdir -p "$backup"
|
|||
say "2/5 Stack stoppen und sichern -> backups/$ts"
|
||||
docker compose down
|
||||
cp docker-compose.yml "$backup/docker-compose.yml"
|
||||
# Die Datenverzeichnisse gehoeren root (der Container schreibt als root), deshalb wird
|
||||
# im Container gepackt statt auf dem Host — spart sudo.
|
||||
docker run --rm -v "$REPO:/work" -w /work --entrypoint tar \
|
||||
"lfnovo/open_notebook@$pinned" -czf "backups/$ts/data.tgz" surreal_data notebook_data
|
||||
# Direkt auf dem Host, weil die Container als Host-User laufen (user: "1000:1000")
|
||||
# und die Daten uns gehoeren. Liefen sie als root, scheiterte das an den Rechten.
|
||||
tar -czf "$backup/data.tgz" surreal_data notebook_data
|
||||
echo " $(du -sh "$backup/data.tgz" | cut -f1) gesichert"
|
||||
|
||||
restore() {
|
||||
say "ROLLBACK"
|
||||
docker compose down || true
|
||||
cp "$backup/docker-compose.yml" docker-compose.yml
|
||||
docker run --rm -v "$REPO:/work" -w /work --entrypoint sh \
|
||||
"lfnovo/open_notebook@$pinned" -c \
|
||||
"rm -rf surreal_data notebook_data && tar -xzf backups/$ts/data.tgz"
|
||||
rm -rf surreal_data notebook_data
|
||||
tar -xzf "$backup/data.tgz"
|
||||
docker compose up -d
|
||||
echo " Alter Stand ($pinned) ist wiederhergestellt, Daten aus backups/$ts."
|
||||
echo " Das neue Image bleibt lokal liegen — Ursache pruefen, dann erneut versuchen."
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue