From 30d3127b219ab77cc7eebd7b42a7a2327ba2ed58 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 19:24:44 +0200 Subject: [PATCH] Auf Docker umgestellt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ein Image (pfaedle + pmtiles + Python), zwei Container per compose: web (Port 8080) und updater (stündlich Fahrplan, beim ersten Start Kartenkacheln). install.sh installiert Docker und startet alles. systemd-Dienste entfernt. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01X48pE4bzjHej8w9HyZXCjb --- .dockerignore | 5 ++ .env.example | 3 ++ .gitignore | 2 + Dockerfile | 28 ++++++++++ README.md | 46 +++++++++------- compose.yaml | 21 ++++++++ deploy/install.sh | 91 ++++++++------------------------ deploy/trainpulse-update.service | 12 ----- deploy/trainpulse-update.timer | 10 ---- deploy/trainpulse.service | 15 ------ trainpulse/config.py | 2 + trainpulse/prepare.py | 20 +++++++ 12 files changed, 132 insertions(+), 123 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 Dockerfile create mode 100644 compose.yaml delete mode 100644 deploy/trainpulse-update.service delete mode 100644 deploy/trainpulse-update.timer delete mode 100644 deploy/trainpulse.service diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2705589 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.venv +.git +data +.env +**/__pycache__ diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..363d1d8 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +# API-Key von https://api-manager.opentransportdata.swiss (Produkt "GTFS-RT"). +# Leer lassen = Karte läuft nur nach Fahrplan, ohne Verspätungen. +TRAINPULSE_RT_KEY= diff --git a/.gitignore b/.gitignore index 77ac754..f8f9839 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .venv/ __pycache__/ *.pyc +data/ +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fa29468 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# pfaedle: rechnet aus OpenStreetMap die echten Streckenverläufe +FROM debian:trixie AS pfaedle +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates git cmake g++ make libzip-dev zlib1g-dev libbz2-dev \ + && git clone --depth 1 --recurse-submodules --shallow-submodules https://github.com/ad-freiburg/pfaedle /src \ + && cmake -S /src -B /build -DCMAKE_BUILD_TYPE=Release \ + && make -C /build -j"$(nproc)" pfaedle \ + && make -C /build install + +# pmtiles: schneidet die Kartenkacheln für die Region aus +FROM golang:1.25-trixie AS pmtiles +RUN go install github.com/protomaps/go-pmtiles@v1.31.2 + +FROM python:3.13-slim-trixie +RUN apt-get update && apt-get install -y --no-install-recommends libzip5 zlib1g libbz2-1.0 libgomp1 \ + && rm -rf /var/lib/apt/lists/* +COPY --from=pfaedle /usr/local/bin/pfaedle /usr/local/bin/ +COPY --from=pfaedle /usr/local/etc/pfaedle /usr/local/etc/pfaedle +COPY --from=pmtiles /go/bin/go-pmtiles /usr/local/bin/pmtiles +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY trainpulse trainpulse +COPY web web +COPY tests tests +ENV TRAINPULSE_DATA=/data PYTHONUNBUFFERED=1 +EXPOSE 8080 +CMD ["uvicorn", "trainpulse.server:app", "--host", "0.0.0.0", "--port", "8080", "--proxy-headers"] diff --git a/README.md b/README.md index e277151..465fd4c 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,11 @@ Ziel, Verspätung und Fahrtverlauf mit Gleis. Hintergrund umschaltbar zwischen K ## Wie es funktioniert ``` -opentransportdata.swiss ──GTFS (Mo+Do)──▶ trainpulse-update (stündlich) +opentransportdata.swiss ──GTFS (Mo+Do)──▶ updater-Container (stündlich) ├─ Filter: nur Bahn (route_type 100–199) OpenStreetMap (Geofabrik) ─────────────────▶├─ pfaedle: echte Streckenverläufe - └─▶ /var/lib/trainpulse/feeds// -opentransportdata.swiss ──GTFS-RT (35 s)──▶ trainpulse (Webserver, Port 8080) + └─▶ data/feeds// +opentransportdata.swiss ──GTFS-RT (35 s)──▶ web-Container (Port 8080) ├─ Position = Fahrplan + Verspätung, entlang Strecke └─ /api/vehicles, /api/trip/…, /api/status, Weboberfläche ``` @@ -23,33 +23,43 @@ Wechsel des Fahrplans: Neue Fahrpläne erscheinen Montag und Donnerstag zwischen die Echtzeitdaten stellen erst um 15 Uhr um. Der Server lädt deshalb immer den Fahrplan, auf den sich die Echtzeitdaten gerade beziehen (`feed_version`), und schaltet automatisch mit um. -## Installation auf Proxmox +## Installation auf Proxmox (Docker) + +TrainPulse läuft als zwei Docker-Container aus demselben Image: +`web` (Webserver, Port 8080) und `updater` (holt stündlich neue Fahrpläne und beim ersten Start +die Kartenkacheln). Alle Daten liegen im Ordner `data/` neben der `compose.yaml`. 1. **Container anlegen** (Proxmox-Oberfläche → *Create CT*): - Debian 12 oder 13 Template, unprivileged, 2 Kerne, **4 GB RAM** (pfaedle braucht beim Aufbereiten bis ~2 GB, der Server ~0,5 GB), 20 GB Disk, Netz per DHCP. -2. **Projekt in den Container kopieren**, z. B. `scp trainpulse.zip root@:` und dort - `apt install -y unzip && unzip trainpulse.zip`. -3. **Installieren:** `bash trainpulse/deploy/install.sh` - Baut pfaedle, lädt die Kartenkacheln für die Schweiz, richtet die Dienste ein und startet - die erste Fahrplan-Aufbereitung im Hintergrund (`journalctl -fu trainpulse-update`). -4. **Echtzeit freischalten:** Auf registrieren, + Debian 12 oder 13 Template, unprivileged, 2 Kerne, **4 GB RAM** (pfaedle braucht beim + Aufbereiten bis ~2 GB, der Server ~0,5 GB), **20 GB Disk**, Netz per DHCP. + Unter *Options → Features* **Nesting** und **keyctl** aktivieren – ohne das läuft Docker + im LXC nicht. +2. **Installieren** (im Container als root): + ```bash + apt install -y git + git clone https://git.scarriffle.com/Scarriffle/Trainpulse.git + bash Trainpulse/deploy/install.sh + ``` + Das Skript installiert Docker, falls es fehlt, baut das Image und startet beide Container. +3. **Echtzeit freischalten:** Auf registrieren, eine Applikation anlegen, das Produkt *GTFS-RT* abonnieren und den API-Key in - `/etc/trainpulse.env` bei `TRAINPULSE_RT_KEY=` eintragen. Dann `systemctl restart trainpulse`. + `Trainpulse/.env` bei `TRAINPULSE_RT_KEY=` eintragen. Dann `docker compose up -d`. Danach ist die Karte unter `http://:8080` erreichbar. Für den Zugriff von aussen einfach hinter deinen bestehenden Reverse Proxy hängen. -Ein Update des Programms: neue Version in den Container kopieren, `install.sh` nochmals ausführen. +**Update:** `git pull && bash deploy/install.sh` – baut das Image neu, Daten bleiben. -## Nützliche Befehle +## Nützliche Befehle (im Ordner `Trainpulse`) | Was | Befehl | |---|---| | Status | `curl localhost:8080/api/status` | -| Logs Webserver | `journalctl -fu trainpulse` | -| Logs Fahrplan-Update | `journalctl -fu trainpulse-update` | -| Fahrplan sofort neu aufbereiten | `sudo -u trainpulse env $(cat /etc/trainpulse.env) /opt/trainpulse/.venv/bin/python -m trainpulse.prepare --force` | -| Selbsttest | `/opt/trainpulse/.venv/bin/python tests/test_core.py` (im Projektordner) | +| Logs Webserver | `docker compose logs -f web` | +| Logs Fahrplan-Update | `docker compose logs -f updater` | +| Fahrplan sofort neu aufbereiten | `docker compose exec updater python -m trainpulse.prepare --force` | +| Selbsttest | `docker compose exec web python tests/test_core.py` | +| Stoppen / Starten | `docker compose down` / `docker compose up -d` | ## Datenquellen und Lizenzen diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..1efd6f0 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,21 @@ +services: + web: + build: . + image: trainpulse + restart: unless-stopped + ports: + - "8080:8080" + env_file: .env + volumes: + - ./data:/data + + # holt stündlich neue Fahrpläne (und beim ersten Start die Kartenkacheln) + updater: + build: . + image: trainpulse + depends_on: [web] + restart: unless-stopped + env_file: .env + volumes: + - ./data:/data + command: ["sh", "-c", "while true; do python -m trainpulse.prepare; sleep 3600; done"] diff --git a/deploy/install.sh b/deploy/install.sh index 2ba62e3..6e25acc 100644 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -1,87 +1,42 @@ #!/usr/bin/env bash -# TrainPulse – Installation in einem Debian-LXC (Proxmox). Im Container als root: +# TrainPulse – Installation mit Docker (z. B. in einem Debian-LXC auf Proxmox). Als root: # bash deploy/install.sh -# Erneut ausführen = Update des Programmcodes (Daten, Karte und Einstellungen bleiben). +# Erneut ausführen = Update (Image neu bauen, Daten und Einstellungen bleiben). set -euo pipefail - -APP=/opt/trainpulse -DATA=/var/lib/trainpulse -SRC=$(cd "$(dirname "$0")/.." && pwd) -BBOX=5.8,45.7,10.6,47.9 # Schweiz mit etwas Rand +cd "$(dirname "$0")/.." [ "$(id -u)" = 0 ] || { echo "Bitte als root ausführen" >&2; exit 1; } command -v apt-get >/dev/null || { echo "Nur für Debian/Ubuntu (apt) gedacht" >&2; exit 1; } -command -v systemctl >/dev/null || { echo "systemd fehlt – im Proxmox-LXC mit Debian-Template ist es dabei" >&2; exit 1; } mem=$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo) [ "$mem" -ge 3500 ] || echo "WARNUNG: nur ${mem} MB RAM – für die Fahrplan-Aufbereitung werden ~4 GB empfohlen" >&2 -echo "==> Pakete (Python, Compiler und Bibliotheken für pfaedle, Werkzeuge)" -export DEBIAN_FRONTEND=noninteractive -apt-get update -qq -apt-get install -y -qq python3 python3-venv python3-pip git cmake g++ make \ - libzip-dev zlib1g-dev libbz2-dev curl ca-certificates tar >/dev/null - -echo "==> Programm nach $APP" -id trainpulse &>/dev/null || useradd --system --home-dir "$DATA" --shell /usr/sbin/nologin trainpulse -mkdir -p "$APP" "$DATA/tiles" -rm -rf "$APP/trainpulse" "$APP/web" -cp -r "$SRC/trainpulse" "$SRC/web" "$SRC/requirements.txt" "$APP/" -[ -d "$APP/.venv" ] || python3 -m venv "$APP/.venv" -"$APP/.venv/bin/pip" install -q --upgrade -r "$APP/requirements.txt" - -if ! command -v pfaedle >/dev/null; then - echo "==> pfaedle bauen (Streckenverläufe aus OpenStreetMap, dauert ein paar Minuten)" - tmp=$(mktemp -d) - git clone -q --depth 1 --recurse-submodules --shallow-submodules https://github.com/ad-freiburg/pfaedle "$tmp/src" - cmake -S "$tmp/src" -B "$tmp/build" -DCMAKE_BUILD_TYPE=Release >/dev/null - make -C "$tmp/build" -j"$(nproc)" >/dev/null - make -C "$tmp/build" install >/dev/null - rm -rf "$tmp" +if ! command -v docker >/dev/null || ! docker compose version >/dev/null 2>&1; then + echo "==> Docker installieren (offizielles Skript von get.docker.com)" + apt-get update -qq + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq curl ca-certificates >/dev/null + curl -fsSL https://get.docker.com | sh fi - -if ! command -v pmtiles >/dev/null; then - echo "==> pmtiles-Werkzeug" - url=$(curl -fsSL https://api.github.com/repos/protomaps/go-pmtiles/releases/latest | python3 -c \ - 'import json,sys; print(next(a["browser_download_url"] for a in json.load(sys.stdin)["assets"] if a["name"].endswith("Linux_x86_64.tar.gz")))') - curl -fsSL "$url" | tar xz -C /usr/local/bin pmtiles -fi - -if [ ! -s "$DATA/tiles/region.pmtiles" ]; then - echo "==> Kartenkacheln (OpenStreetMap via Protomaps) für $BBOX" - for i in 1 2 3 4 5 6 7; do - d=$(date -d "-$i day" +%Y%m%d) - if pmtiles extract "https://build.protomaps.com/$d.pmtiles" "$DATA/tiles/region.tmp.pmtiles" --bbox=$BBOX --maxzoom=15; then - mv "$DATA/tiles/region.tmp.pmtiles" "$DATA/tiles/region.pmtiles" - break - fi - done - [ -s "$DATA/tiles/region.pmtiles" ] || { echo "Kartenkacheln konnten nicht geladen werden" >&2; exit 1; } -fi - -if [ ! -f /etc/trainpulse.env ]; then - cat > /etc/trainpulse.env </dev/null 2>&1; then + cat >&2 <<'EOF' +Docker ist installiert, kann aber keine Container starten. +In einem Proxmox-LXC: Container herunterfahren, unter Optionen → Features +"Nesting" und "keyctl" aktivieren, Container starten, Skript nochmals ausführen. EOF - chmod 640 /etc/trainpulse.env - chgrp trainpulse /etc/trainpulse.env + exit 1 fi -chown -R trainpulse:trainpulse "$DATA" -echo "==> Dienste" -cp "$SRC"/deploy/trainpulse.service "$SRC"/deploy/trainpulse-update.service "$SRC"/deploy/trainpulse-update.timer /etc/systemd/system/ -systemctl daemon-reload -systemctl enable --now trainpulse-update.timer -systemctl enable trainpulse.service -systemctl restart trainpulse.service -systemctl start --no-block trainpulse-update.service +[ -f .env ] || cp .env.example .env +mkdir -p data + +echo "==> Image bauen und starten (das erste Mal dauert der Bau von pfaedle ein paar Minuten)" +docker compose up -d --build ip=$(hostname -I | awk '{print $1}') cat < None: src.unlink() +def ensure_tiles() -> None: + """Kartenkacheln (OpenStreetMap, von Protomaps) einmalig für die Region herunterladen.""" + dest = C.DATA_DIR / "tiles" / "region.pmtiles" + if dest.exists() and dest.stat().st_size: + return + dest.parent.mkdir(parents=True, exist_ok=True) + tmp = dest.with_suffix(".tmp") + for days in range(1, 8): # Protomaps baut täglich, ältere Builds werden gelöscht + d = (dt.date.today() - dt.timedelta(days=days)).strftime("%Y%m%d") + log.info("Lade Kartenkacheln vom %s …", d) + r = subprocess.run([C.PMTILES, "extract", f"https://build.protomaps.com/{d}.pmtiles", str(tmp), + f"--bbox={C.TILES_BBOX}", "--maxzoom=15"]) + if r.returncode == 0 and tmp.exists() and tmp.stat().st_size: + tmp.rename(dest) + return + log.error("Kartenkacheln konnten nicht geladen werden – Karte bleibt ohne Hintergrund") + + def publish(src: Path, meta: dict) -> None: (src / "meta.json").write_text(json.dumps(meta)) dest = C.FEEDS_DIR / meta["feed_version"] @@ -219,6 +237,8 @@ def main() -> None: logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") C.FEEDS_DIR.mkdir(parents=True, exist_ok=True) + if not args.zip: + ensure_tiles() state_file = C.DATA_DIR / "state.json" state = json.loads(state_file.read_text()) if state_file.exists() else {} work = C.DATA_DIR / "work"