Fuer Apps, die heute per SCP deployt werden. Schwerpunkt liegt auf den beiden Aenderungen, die nicht rueckwirkungsfrei sind: der feste Projektname benennt die Volumes um, der feste Container-Name kollidiert mit dem noch laufenden alten Stack. Enthaelt das Rezept zum Umkopieren der Volumes und einen Rueckweg, solange das alte Volume noch existiert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
188 lines
6.0 KiB
Markdown
188 lines
6.0 KiB
Markdown
# 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 |
|