SendSecret: Erstimport und Umstellung auf Tag-Deployment
Wrapper-Frontend vor cryptgeon: der Browser verschluesselt lokal, per Mail geht nur der Link raus. Fuer das Deployment nach den Konventionen aus DEPLOY.md hergerichtet: - docker-compose.yml mit festem Projekt- und Container-Namen, kein ports-Mapping, Healthcheck als Deploy-Gate. cryptgeon und redis liegen im internen Netz, nur app haengt im Web-Netz. - cryptgeon von latest auf 2.9.3 gepinnt. Das ist derselbe Stand, den latest bisher geliefert hat; 2.6.2 existiert nicht. - /healthz in server.js, vor dem Catch-all-Proxy registriert. - Dockerfile auf npm ci mit Lockfile und non-root umgestellt. - .gitea/workflows/deploy.yml: Build und Syntaxpruefung vor dem Deploy, .env aus dem Repo-Secret DOTENV. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
187
DEPLOY.md
Normal file
187
DEPLOY.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# Deployment-Konventionen
|
||||
|
||||
Verbindlich fuer jede App, die auf dem VPS per Tag-Push deployt wird.
|
||||
Abweichungen brauchen einen Grund, der hier nicht steht.
|
||||
|
||||
## Ablauf
|
||||
|
||||
```
|
||||
git tag v1.2.3 && git push --tags
|
||||
-> Gitea Actions (act_runner auf dem VPS)
|
||||
-> npm ci / build / test <- rot bricht ab, nichts wird deployt
|
||||
-> .env aus Repo-Secret DOTENV
|
||||
-> docker compose up -d --build --wait
|
||||
```
|
||||
|
||||
Kein SCP, kein manuelles Neubauen. Der Code landet nie im Dateisystem des
|
||||
Hosts - der Job-Container klont, baut gegen den Docker-Daemon des Hosts und
|
||||
startet den Container.
|
||||
|
||||
## Infrastruktur (existiert bereits, nicht neu anlegen)
|
||||
|
||||
| Ding | Wert |
|
||||
|---|---|
|
||||
| Git | Gitea, `https://gitea.lucas-orth.de`, Container `gitea`, intern `:3000` |
|
||||
| Runner | `act_runner`, Container `gitea-runner`, Label `ubuntu-latest` |
|
||||
| Netz CI | `gitea-ci` - Gitea + Runner + Job-Container |
|
||||
| Netz Web | `nginx-proxy-manager_default` - Proxy + alle App-Container |
|
||||
| Job-Image | `catthehacker/ubuntu:act-latest` (node + docker-cli + compose) |
|
||||
|
||||
Die beiden Netze sind bewusst getrennt: der Runner spricht mit Gitea, nicht
|
||||
mit dem Proxy. App-Container haengen im Web-Netz, nicht im CI-Netz.
|
||||
|
||||
## docker-compose.yml
|
||||
|
||||
```yaml
|
||||
# Fester Projektname. Ohne das leitet Compose ihn aus dem Verzeichnisnamen
|
||||
# ab - und der ist im CI-Job ein anderer als auf dem Host.
|
||||
name: <app>
|
||||
|
||||
services:
|
||||
app:
|
||||
# Versionstag ermoeglicht Rollback ohne Rebuild.
|
||||
image: <app>:${IMAGE_TAG:-latest}
|
||||
build: .
|
||||
# Fester Name: das ist die Adresse, auf die der Proxy zeigt.
|
||||
container_name: <app>
|
||||
restart: unless-stopped
|
||||
# Wird im Workflow aus dem Secret DOTENV erzeugt.
|
||||
env_file: .env
|
||||
networks:
|
||||
- nginx-proxy-manager_default
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:<port>/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
||||
interval: 60s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
networks:
|
||||
nginx-proxy-manager_default:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
<app>-data:
|
||||
# Fester Name, damit das Volume nicht am Projektnamen haengt.
|
||||
name: <app>-data
|
||||
```
|
||||
|
||||
**Kein `ports:`.** Der Proxy erreicht den Container ueber das gemeinsame Netz
|
||||
unter seinem Container-Namen. Ein Port-Mapping wuerde den Dienst zusaetzlich
|
||||
offen ans Internet haengen.
|
||||
|
||||
**Healthcheck ist Pflicht.** Der Deploy-Schritt nutzt `--wait` und macht ihn
|
||||
zum Gate. Ohne Healthcheck wird der Lauf gruen, auch wenn der Container in
|
||||
einer Crash-Schleife haengt.
|
||||
|
||||
**Keine relativen Bind-Mounts** (`./data:/data`). Compose laeuft im
|
||||
Job-Container, der Pfad wuerde auf dem Host ins Leere zeigen. Persistente
|
||||
Daten gehoeren in ein named volume mit festem `name:`.
|
||||
|
||||
## .gitea/workflows/deploy.yml
|
||||
|
||||
```yaml
|
||||
name: Build & Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Intern klonen. Die oeffentliche Domain ist aus Containern heraus
|
||||
# NICHT erreichbar: Gitea laeuft auf demselben Host, das Paket geht
|
||||
# an die oeffentliche IP und findet nicht zurueck (NAT-Hairpin).
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
github-server-url: http://gitea:3000
|
||||
|
||||
# --- Build & Tests, projektspezifisch ---
|
||||
|
||||
- name: Tag ermitteln
|
||||
run: echo "IMAGE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
|
||||
- name: .env aus Secret erzeugen
|
||||
env:
|
||||
DOTENV: ${{ secrets.DOTENV }}
|
||||
run: |
|
||||
if [ -z "$DOTENV" ]; then
|
||||
echo "Secret DOTENV ist leer oder nicht gesetzt."
|
||||
exit 1
|
||||
fi
|
||||
umask 077
|
||||
echo "$DOTENV" > .env
|
||||
|
||||
- name: Deployen
|
||||
run: docker compose up -d --build --remove-orphans --wait --wait-timeout 180
|
||||
|
||||
- name: Alte Layer aufraeumen
|
||||
run: docker image prune -f
|
||||
```
|
||||
|
||||
Tests gehoeren **vor** den Deploy-Schritt. Ein roter Lauf darf den Server
|
||||
nicht anfassen.
|
||||
|
||||
## Dockerfile
|
||||
|
||||
- Multi-Stage: Build-Abhaengigkeiten landen nicht im finalen Image.
|
||||
- `USER node` bzw. non-root im finalen Stage.
|
||||
- `EXPOSE <port>` dokumentiert den internen Port (oeffnet nichts).
|
||||
- Ein Health-Endpoint (`/healthz`), auf den der Healthcheck zeigt.
|
||||
|
||||
## Secrets
|
||||
|
||||
Ein einziges Repo-Secret **`DOTENV`** mit dem kompletten `.env`-Inhalt,
|
||||
mehrzeilig. Anlegen unter Repo -> Einstellungen -> Actions -> Secrets.
|
||||
|
||||
Gitea speichert Secrets write-only: nach dem Anlegen nicht mehr lesbar, nur
|
||||
ersetzbar. Die kanonische Fassung gehoert deshalb in den Passwortmanager,
|
||||
Gitea haelt nur die Arbeitskopie fuer die Pipeline.
|
||||
|
||||
Der Wert wird ueber `env:` an die Shell gereicht, nie direkt interpoliert -
|
||||
sonst zerlegt der Shell-Parser Werte mit `$`, Backticks oder Quotes.
|
||||
|
||||
## Nginx Proxy Manager
|
||||
|
||||
Neuer Proxy Host:
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| Domain Names | `<app>.lucas-orth.de` |
|
||||
| Scheme | `http` |
|
||||
| Forward Hostname / IP | `<app>` (= `container_name`) |
|
||||
| Forward Port | `<port>` |
|
||||
|
||||
Danach Let's-Encrypt-Zertifikat ausstellen und "Force SSL" aktivieren.
|
||||
|
||||
Weil der Container-Name die Adresse ist: aendert sich `container_name`,
|
||||
zeigt der Proxy-Host ins Leere.
|
||||
|
||||
## Bestehende App umstellen
|
||||
|
||||
Laeuft die App heute schon manuell auf dem Server, gilt statt des folgenden
|
||||
Abschnitts [MIGRATION.md](MIGRATION.md) - dort steht insbesondere, wie die
|
||||
Volumes den Wechsel des Projektnamens ueberleben.
|
||||
|
||||
## Erstes Deployment einer App
|
||||
|
||||
1. Repo in Gitea anlegen, pushen.
|
||||
2. Secret `DOTENV` setzen.
|
||||
3. Pruefen, ob schon ein Container mit dem Namen laeuft
|
||||
(`docker ps -a --filter name=<app>`). Falls ja: im alten Verzeichnis
|
||||
einmal `docker compose down`, sonst kollidiert der feste Container-Name.
|
||||
4. Proxy Host im NPM anlegen.
|
||||
5. `git tag v1.0.0 && git push origin v1.0.0`
|
||||
|
||||
## Fallstricke
|
||||
|
||||
| Symptom | Ursache |
|
||||
|---|---|
|
||||
| Job haengt beim Checkout | `github-server-url` fehlt, klont ueber die oeffentliche Domain |
|
||||
| `network not found` | App-Container im CI-Netz statt im Web-Netz (oder umgekehrt) |
|
||||
| Zweiter Container statt Update | `name:` im Compose fehlt, Projektname weicht ab |
|
||||
| Lauf gruen, App tot | `--wait` fehlt oder kein Healthcheck definiert |
|
||||
| Volume leer nach Umstellung | Projektname geaendert, altes Volume hiess anders |
|
||||
| Proxy liefert 502 | `container_name` geaendert oder Container nicht im Web-Netz |
|
||||
Reference in New Issue
Block a user