Deploy per Gitea Actions auf 192.168.1.5
Deploy auf den Timer-Server / deploy (push) Failing after 2s

Push auf main rollt den Stand selbst aus, statt ihn von Hand zu kopieren.
Der Runner (pve-runner) liegt im selben LAN wie der Zielhost.

Drei Dinge, die dabei nicht passieren duerfen und deshalb abgesichert sind:

- Die .env, das data-Verzeichnis und node_modules sind vom rsync-Abgleich
  ausgenommen. Das --delete raeumt zwar geloeschte Dateien weg, laesst diese
  Pfade aber in Ruhe (kein --delete-excluded).
- npm ci laeuft auf dem Zielhost, nicht im Runner-Container: bcrypt und
  sqlite3 sind native Module und muessen zur dortigen Umgebung passen.
  Aufgerufen wird es nur, wenn sich backend/package*.json geaendert hat.
- Neu gestartet wird nur bei Aenderungen unter backend/ oder deploy/.
  Eine geaenderte Frontend-Datei wird ausgeliefert, ohne den laufenden
  Dienst anzufassen.

Danach ein Health-Check gegen /health; bleibt die Antwort aus, schlaegt der
Lauf fehl und die letzten Journal-Zeilen stehen im Protokoll.

Die Einrichtung (Deploy-Benutzer, eng gefasste sudo-Regel, Variablen und
Secrets) steht in DEPLOY.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-04 17:56:35 +02:00
co-authored by Claude Opus 5
parent a7e77793a8
commit 1cd966a208
2 changed files with 187 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
name: Deploy auf den Timer-Server
# Laeuft bei jedem Push auf main und laesst sich in der Gitea-Oberflaeche
# ("Actions" -> Workflow -> "Run workflow") auch von Hand ausloesen.
on:
push:
branches: [main]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
env:
# Repository-Variablen (Settings -> Actions -> Variables)
DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}
DEPLOY_USER: ${{ vars.DEPLOY_USER }}
DEPLOY_PATH: ${{ vars.DEPLOY_PATH }}
DEPLOY_SERVICE: ${{ vars.DEPLOY_SERVICE }}
DEPLOY_OWNER: ${{ vars.DEPLOY_OWNER }}
HEALTH_URL: ${{ vars.HEALTH_URL }}
# Repository-Secrets (Settings -> Actions -> Secrets)
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
steps:
- name: Code auschecken
uses: actions/checkout@v4
- name: Werkzeuge installieren
run: |
apt-get update -qq
apt-get install -y -qq rsync openssh-client
- name: Konfiguration pruefen
run: |
fehlt=""
for v in DEPLOY_HOST DEPLOY_USER DEPLOY_PATH DEPLOY_SERVICE DEPLOY_SSH_KEY DEPLOY_KNOWN_HOSTS; do
if [ -z "${!v:-}" ]; then fehlt="$fehlt $v"; fi
done
if [ -n "$fehlt" ]; then
echo "Es fehlen noch Variablen/Secrets:$fehlt"
echo "Nachzutragen unter Settings -> Actions -> Variables bzw. Secrets."
exit 1
fi
echo "Ziel: $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH (Dienst: $DEPLOY_SERVICE)"
- name: SSH einrichten
run: |
mkdir -p ~/.ssh && chmod 700 ~/.ssh
printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519
printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/known_hosts
- name: Dateien uebertragen
run: |
# --delete raeumt geloeschte Dateien auf dem Server weg. Die
# ausgeschlossenen Pfade sind davon ausgenommen (kein --delete-excluded!):
# Konfiguration, Datenbank und node_modules bleiben unangetastet.
rsync -rlpt --delete --out-format='%n' \
--exclude='.git/' --exclude='.gitea/' \
--exclude='.env' --exclude='.env.*' \
--exclude='data/' --exclude='node_modules/' \
-e 'ssh -o BatchMode=yes' \
./ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/" | tee uebertragen.txt
- name: Abhaengigkeiten, Neustart und Health-Check
run: |
# Neu installiert wird nur, wenn sich die Paketliste geaendert hat.
# bcrypt und sqlite3 sind native Module: npm ci gehoert deshalb auf
# den Zielhost und nicht in den Runner-Container.
NPM=nein
RESTART=nein
if grep -q 'backend/package' uebertragen.txt; then NPM=ja; fi
if grep -qE '^(backend/|deploy/)' uebertragen.txt; then RESTART=ja; fi
if [ ! -s uebertragen.txt ]; then echo "Nichts geaendert."; fi
echo "npm ci: $NPM | Neustart: $RESTART"
ssh -o BatchMode=yes "$DEPLOY_USER@$DEPLOY_HOST" \
"NPM='$NPM' RESTART='$RESTART' \
DEPLOY_PATH='$DEPLOY_PATH' DEPLOY_SERVICE='$DEPLOY_SERVICE' \
DEPLOY_OWNER='${DEPLOY_OWNER:-timerapp:timerapp}' \
HEALTH_URL='${HEALTH_URL:-http://127.0.0.1:3003/health}' \
bash -euo pipefail -s" <<'REMOTE'
SUDO=""; [ "$(id -u)" -eq 0 ] || SUDO="sudo -n"
if [ "$NPM" = ja ]; then
echo "--- npm ci (nur Produktionsabhaengigkeiten)"
cd "$DEPLOY_PATH/backend"
$SUDO npm ci --omit=dev --no-audit --no-fund
fi
# Alles unter dem Anwendungsverzeichnis gehoert dem Dienstbenutzer.
$SUDO chown -R "$DEPLOY_OWNER" "$DEPLOY_PATH"
if [ "$RESTART" = nein ]; then
echo "--- Keine Backend-Aenderung: Neustart nicht noetig"
exit 0
fi
echo "--- Dienst neu starten: $DEPLOY_SERVICE"
$SUDO systemctl restart "$DEPLOY_SERVICE"
echo "--- Health-Check"
for i in $(seq 1 15); do
if curl -fsS --max-time 3 "$HEALTH_URL" >/dev/null 2>&1; then
echo "Dienst antwortet:"; curl -fsS "$HEALTH_URL"; echo
exit 0
fi
sleep 2
done
echo "Dienst antwortet nach 30 s nicht. Letzte Zeilen aus dem Journal:"
$SUDO journalctl -u "$DEPLOY_SERVICE" -n 40 --no-pager || true
exit 1
REMOTE
+70
View File
@@ -475,3 +475,73 @@ CSV, Filter, Löschen, Abmelden) sowie ein Durchlauf im echten Browser.
Die App ist klein genug, um sie vollständig zu lesen: `server.js` ~470 Zeilen, Die App ist klein genug, um sie vollständig zu lesen: `server.js` ~470 Zeilen,
`app.js` ~460 Zeilen, beide durchgehend kommentiert und auf Deutsch beschriftet. `app.js` ~460 Zeilen, beide durchgehend kommentiert und auf Deutsch beschriftet.
Im Zweifel ist das Lesen der Quelle schneller als das Raten. Im Zweifel ist das Lesen der Quelle schneller als das Raten.
---
## Automatischer Deploy per Gitea Actions
Jeder Push auf `main` in `git.handy-insel.de/kerimatilgan/timer-app` rollt den
Stand selbsttaetig auf den Server aus. Der Workflow liegt in
`.gitea/workflows/deploy.yaml` und laesst sich in der Gitea-Oberflaeche unter
"Actions" auch von Hand starten.
### Was der Workflow tut
1. Uebertraegt die versionierten Dateien per `rsync` nach `/opt/timer-app`.
**Nicht angefasst** werden `backend/.env`, `data/` und `node_modules/` —
sie sind vom Abgleich ausgenommen und ueberleben auch das `--delete`,
das geloeschte Dateien auf dem Server aufraeumt.
2. `npm ci --omit=dev` **nur**, wenn sich `backend/package*.json` geaendert hat.
Das laeuft bewusst auf dem Zielhost: `bcrypt` und `sqlite3` sind native
Module und muessen zur dortigen Architektur passen.
3. Neustart des Dienstes **nur**, wenn sich etwas unter `backend/` oder
`deploy/` geaendert hat. Reine Frontend-Aenderungen brauchen keinen.
4. Health-Check gegen `/health` (15 Versuche, 30 s). Antwortet der Dienst
nicht, schlaegt der Lauf fehl und die letzten 40 Journal-Zeilen stehen
im Protokoll.
### Einmalige Einrichtung
Auf dem Zielhost einen Deploy-Benutzer anlegen, ihm das Verzeichnis geben und
den Neustart des einen Dienstes erlauben:
```bash
useradd -r -m -s /bin/bash deploy
mkdir -p /home/deploy/.ssh && chmod 700 /home/deploy/.ssh
echo '<oeffentlicher Schluessel>' >> /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh && chmod 600 /home/deploy/.ssh/authorized_keys
# Schreibrecht auf das Anwendungsverzeichnis
chown -R deploy:timerapp /opt/timer-app && chmod -R g+rX /opt/timer-app
# Genau die drei Befehle ohne Passwort - nicht mehr
cat > /etc/sudoers.d/deploy-timer-app <<'EOF'
deploy ALL=(root) NOPASSWD: /bin/systemctl restart timer-app, \
/bin/journalctl -u timer-app *, \
/bin/chown -R timerapp\:timerapp /opt/timer-app
EOF
chmod 440 /etc/sudoers.d/deploy-timer-app
visudo -c
```
Im Repo unter **Settings -> Actions** hinterlegen:
| Art | Name | Wert |
|---|---|---|
| Variable | `DEPLOY_HOST` | `192.168.1.5` |
| Variable | `DEPLOY_USER` | `deploy` |
| Variable | `DEPLOY_PATH` | `/opt/timer-app` |
| Variable | `DEPLOY_SERVICE` | `timer-app` |
| Variable | `DEPLOY_OWNER` | `timerapp:timerapp` |
| Variable | `HEALTH_URL` | `http://127.0.0.1:3003/health` |
| Secret | `DEPLOY_SSH_KEY` | privater Deploy-Schluessel (ganze Datei inkl. BEGIN/END-Zeilen) |
| Secret | `DEPLOY_KNOWN_HOSTS` | `ssh-keyscan 192.168.1.5` — Zeile(n) fuer den Zielhost |
### Grenzen
- Die `.env` wird **nie** ausgerollt. Kommt eine neue Einstellung dazu, gehoert
sie von Hand in die `.env` auf dem Server (Vorlage: `backend/.env.example`),
sonst startet der Dienst nach dem Deploy womoeglich nicht.
- Es gibt keinen automatischen Rollback. Faellt der Health-Check durch, laeuft
der Dienst weiter im Fehlerzustand — der Lauf ist rot und das Journal steht
im Protokoll. Zurueck geht es mit einem Revert-Commit auf `main`.