ci: Gitea Actions deploy for test (main) and prod (tags) stacks

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
JannisandClaude Sonnet 5.5 committed 2026-10-16 21:23:12 +02:00
1 parent 70f1890641
commit aa37b44e00
6 files changed
+195 -4

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
# Deployment per Gitea Actions auf den Docker-Host, auf dem auch der Runner läuft.
# push auf main -> Images bauen, in die Gitea-Registry pushen, Test-Stack aktualisieren
# Tag v* -> dieselben (bereits getesteten) Images als Release taggen, Prod-Stack aktualisieren
# Konfiguration/Secrets liegen auf dem Host in /opt/familyplanner/{test,prod}.env (siehe deploy/env.*.example).
# Runner läuft als Container: Job-Container brauchen Docker-Socket und /opt/familyplanner (rw) – siehe docs/deploy-runner.md.
name: Deploy
on:
push:
branches: [main]
tags: ["v*"]
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: false
env:
CONFIG_DIR: /opt/familyplanner
SERVICES: migrations api web
jobs:
test:
if: github.ref_type == 'branch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Registry-Login
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "${{ vars.REGISTRY_HOST }}" -u "${{ secrets.REGISTRY_USER }}" --password-stdin
- name: Images bauen und pushen
working-directory: deploy
env:
TAG: ${{ github.sha }}
run: |
docker compose --env-file "$CONFIG_DIR/test.env" build $SERVICES
docker compose --env-file "$CONFIG_DIR/test.env" push $SERVICES
- name: Test-Stack aktualisieren
working-directory: deploy
env:
TAG: ${{ github.sha }}
run: |
docker network inspect edge >/dev/null 2>&1 || docker network create edge
docker compose -p familyplanner-test --env-file "$CONFIG_DIR/test.env" pull $SERVICES
docker compose -p familyplanner-test --env-file "$CONFIG_DIR/test.env" up -d --remove-orphans --wait
prod:
if: github.ref_type == 'tag'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Registry-Login
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "${{ vars.REGISTRY_HOST }}" -u "${{ secrets.REGISTRY_USER }}" --password-stdin
- name: Getestete Images zum Release befördern (kein Neubau)
env:
REGISTRY: ${{ vars.REGISTRY_HOST }}/${{ vars.REGISTRY_OWNER }}/
run: |
for svc in migrations api web; do
docker pull "${REGISTRY}familyplanner-$svc:${{ github.sha }}"
docker tag "${REGISTRY}familyplanner-$svc:${{ github.sha }}" "${REGISTRY}familyplanner-$svc:${{ github.ref_name }}"
docker push "${REGISTRY}familyplanner-$svc:${{ github.ref_name }}"
done
- name: Backup vor dem Update
working-directory: deploy
run: |
cp "$CONFIG_DIR/prod.env" .env
docker compose -p familyplanner-prod ps sql --status running -q | grep -q . && COMPOSE_PROJECT_NAME=familyplanner-prod sh scripts/backup.sh || echo "Erster Start, kein Backup"
rm -f .env
- name: Caddyfile auf den Host legen (Bind-Mounts löst der Host-Docker auf)
run: cp deploy/Caddyfile "$CONFIG_DIR/Caddyfile"
- name: Prod-Stack aktualisieren
working-directory: deploy
env:
TAG: ${{ github.ref_name }}
CADDYFILE: /opt/familyplanner/Caddyfile
run: |
docker network inspect edge >/dev/null 2>&1 || docker network create edge
docker compose -p familyplanner-prod --env-file "$CONFIG_DIR/prod.env" --profile proxy pull $SERVICES
docker compose -p familyplanner-prod --env-file "$CONFIG_DIR/prod.env" --profile proxy up -d --remove-orphans --wait
+13 -3
View File
@@ -1,12 +1,22 @@
# Reverse Proxy mit automatischem HTTPS (Let's Encrypt). Domain kommt aus der Umgebungsvariable DOMAIN.
{$DOMAIN} {
# Reverse Proxy mit automatischem HTTPS (Let's Encrypt) für Prod und Test auf demselben Host.
# Caddy läuft nur im Prod-Stack (Profil "proxy") und erreicht beide Stacks über das externe Docker-Netz "edge"
# (Alias <STACK_NAME>-web). Ist der Test-Stack aus, liefert die Test-Domain einfach 502.
(common) {
encode zstd gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
-Server
}
}
{$DOMAIN} {
import common
# WebSockets (SignalR) werden automatisch durchgereicht.
reverse_proxy web:8080
reverse_proxy prod-web:8080
}
{$TEST_DOMAIN} {
import common
reverse_proxy test-web:8080
}
+15 -1
View File
@@ -101,6 +101,12 @@ services:
services__api__http__0: http://api:8080
ports:
- "${WEB_BIND:-127.0.0.1}:${WEB_PORT:-8080}:8080"
# Gemeinsames Netz mit dem Reverse Proxy (Test + Prod auf einem Host): einmalig `docker network create edge`
networks:
default:
edge:
aliases:
- ${STACK_NAME:-prod}-web
depends_on:
api:
condition: service_healthy
@@ -117,19 +123,27 @@ services:
image: caddy:2
environment:
DOMAIN: ${DOMAIN:-localhost}
TEST_DOMAIN: ${TEST_DOMAIN:-test.localhost}
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ${CADDYFILE:-./Caddyfile}:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
networks:
default:
edge:
depends_on:
web:
condition: service_healthy
restart: unless-stopped
networks:
edge:
external: true
volumes:
sql-data:
sql-backup:
+24
View File
@@ -0,0 +1,24 @@
# Auf dem Docker-Host nach /opt/familyplanner/prod.env kopieren und anpassen (nicht einchecken!).
STACK_NAME=prod
REGISTRY=gitea.example.com/owner/
# TAG setzt der Workflow (Release-Tag, z. B. v1.2.0)
WEB_PORT=8080
WEB_BIND=127.0.0.1
# Caddy (Profil "proxy" – nur im Prod-Stack, bedient beide Domains)
DOMAIN=planner.example.com
TEST_DOMAIN=test.planner.example.com
SQL_SA_PASSWORD=
MSSQL_PID=Express
ADMIN_USERNAME=admin
ADMIN_PASSWORD=
ADMIN_EMAIL=
FAMILY_NAME=
VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=
VAPID_SUBJECT=mailto:admin@example.com
GIPHY_API_KEY=
CALDAV_REQUIRE_HTTPS=true
+18
View File
@@ -0,0 +1,18 @@
# Auf dem Docker-Host nach /opt/familyplanner/test.env kopieren und anpassen (nicht einchecken!).
STACK_NAME=test
# Gitea-Registry inkl. Owner und abschließendem Slash
REGISTRY=gitea.example.com/owner/
# TAG setzt der Workflow (Commit-SHA)
WEB_PORT=8081
WEB_BIND=127.0.0.1
SQL_SA_PASSWORD=Test-Anders-als-Prod-2026!
MSSQL_PID=Express
ADMIN_USERNAME=admin
ADMIN_PASSWORD=Test-Erstes-Passwort-2026
ADMIN_EMAIL=
FAMILY_NAME=Testfamilie
# Keine echten Integrationen im Test: VAPID/Giphy leer lassen
CALDAV_REQUIRE_HTTPS=true
+40
View File
@@ -0,0 +1,40 @@
# Runner-Konfiguration für den Deploy (Gitea act_runner als Container)
Der Runner startet jeden Job in einem eigenen Container und spricht dafür mit dem Docker-Daemon des Hosts.
Damit der Deploy-Workflow `docker compose` nutzen kann, braucht der Job-Container den Docker-Socket und das Konfigurationsverzeichnis.
## 1. Runner-Container (Beispiel)
```yaml
services:
runner:
image: gitea/act_runner:latest
restart: unless-stopped
environment:
GITEA_INSTANCE_URL: https://gitea.example.com
GITEA_RUNNER_REGISTRATION_TOKEN: <token>
CONFIG_FILE: /config.yaml
volumes:
- ./config.yaml:/config.yaml
- ./data:/data
- /var/run/docker.sock:/var/run/docker.sock
```
## 2. `config.yaml` des Runners
```yaml
runner:
labels:
# Image mit Docker-CLI + Compose-Plugin
- "ubuntu-latest:docker://catthehacker/ubuntu:act-latest"
container:
options: "-v /var/run/docker.sock:/var/run/docker.sock -v /opt/familyplanner:/opt/familyplanner"
valid_volumes:
- /var/run/docker.sock
- /opt/familyplanner
```
Danach Runner neu starten. `/opt/familyplanner` liegt auf dem **Host** (dort liegen `test.env`, `prod.env`).
Der Pfad muss im Job-Container identisch sein, weil der Host-Daemon Mount-Pfade auflöst.
Hinweis: Jeder Job mit Socket-Zugriff hat Root-Rechte auf dem Host – Runner nur für dieses (private) Repository registrieren.