# 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: business-card-scanner services: app: # Versionstag ermoeglicht Rollback ohne Rebuild. image: business-card-scanner:${IMAGE_TAG:-latest} build: . # Fester Name: das ist die Adresse, auf die der Proxy zeigt. container_name: business-card-scanner 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:8080/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: business-card-scanner-data: # Fester Name, damit das Volume nicht am Projektnamen haengt. name: business-card-scanner-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 8080` 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 | `business-card-scanner.lucas-orth.de` | | Scheme | `http` | | Forward Hostname / IP | `business-card-scanner` (= `container_name`) | | Forward Port | `8080` | 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 Fuer dieses Projekt nicht relevant - es lief nie manuell auf dem Server. ## 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=business-card-scanner`). 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 |