build: add Dockerfile, compose deployment and CI pipeline

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
JannisandClaude Opus 5.5 committed 2026-10-09 18:25:11 +02:00
1 parent 36d64ccf0b
commit 3b36dce605
8 files changed
+334

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
**/bin/
**/obj/
**/TestResults/
.git/
.github/
.vs/
.idea/
artifacts/
tests/
docs/
deploy/
tools/
**/*.user
**/appsettings.Development.json
+63
View File
@@ -0,0 +1,63 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Restore
run: dotnet restore FamilyPlanner.slnx
- name: Build (Warnungen sind Fehler)
run: dotnet build FamilyPlanner.slnx -c Release --no-restore
- name: Unit- und Komponententests
run: |
dotnet test --project tests/FamilyPlanner.UnitTests -c Release --no-build
dotnet test --project tests/FamilyPlanner.Web.Tests -c Release --no-build
- name: HTTPS-Entwicklungszertifikat (für Aspire-Integrationstests)
run: dotnet dev-certs https
- name: Integrationstests (Aspire + SQL-Server-Container)
run: dotnet test --project tests/FamilyPlanner.IntegrationTests -c Release --no-build
docker-images:
runs-on: ubuntu-latest
needs: build-and-test
strategy:
matrix:
project: [FamilyPlanner.Api, FamilyPlanner.Web, FamilyPlanner.MigrationService]
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Image bauen
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
build-args: PROJECT=${{ matrix.project }}
push: false
tags: familyplanner/${{ matrix.project }}:ci
cache-from: type=gha,scope=${{ matrix.project }}
cache-to: type=gha,mode=max,scope=${{ matrix.project }}
+48
View File
@@ -0,0 +1,48 @@
# syntax=docker/dockerfile:1
# Gemeinsames Dockerfile für Api, Web und MigrationService.
# docker build --build-arg PROJECT=FamilyPlanner.Api -t familyplanner-api .
# Alternativ ohne Docker-Build: dotnet publish src/<Projekt> -c Release /t:PublishContainer
ARG DOTNET_VERSION=10.0
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} AS build
ARG PROJECT
WORKDIR /src
# Zuerst nur Projektdateien kopieren, damit der Restore-Layer gecacht wird.
COPY global.json Directory.Build.props Directory.Packages.props .editorconfig ./
COPY src/FamilyPlanner.Domain/*.csproj src/FamilyPlanner.Domain/
COPY src/FamilyPlanner.Shared/*.csproj src/FamilyPlanner.Shared/
COPY src/FamilyPlanner.Infrastructure/*.csproj src/FamilyPlanner.Infrastructure/
COPY src/FamilyPlanner.ServiceDefaults/*.csproj src/FamilyPlanner.ServiceDefaults/
COPY src/FamilyPlanner.Api/*.csproj src/FamilyPlanner.Api/
COPY src/FamilyPlanner.MigrationService/*.csproj src/FamilyPlanner.MigrationService/
COPY src/FamilyPlanner.Web/*.csproj src/FamilyPlanner.Web/
COPY src/FamilyPlanner.Web.Client/*.csproj src/FamilyPlanner.Web.Client/
RUN dotnet restore "src/${PROJECT}/${PROJECT}.csproj"
COPY src/ src/
# Kein --no-restore: das SDK referenziert Microsoft.AspNetCore.App.Internal.Assets (blazor.web.js) erst,
# wenn Razor-Komponenten im Projekt liegen – das ist beim Restore-Layer oben noch nicht der Fall.
RUN dotnet publish "src/${PROJECT}/${PROJECT}.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION} AS final
ARG PROJECT
# curl nur für Docker-Health-Checks.
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=build /app/publish .
ENV ASPNETCORE_HTTP_PORTS=8080 \
DOTNET_RUNNING_IN_CONTAINER=true \
APP_DLL=${PROJECT}.dll
EXPOSE 8080
# Non-Root: der Benutzer "app" (UID 1654) ist in den Microsoft-Images vorhanden.
USER $APP_UID
ENTRYPOINT ["sh", "-c", "exec dotnet \"$APP_DLL\""]
+26
View File
@@ -0,0 +1,26 @@
# Kopieren nach deploy/.env und anpassen. Die .env-Datei niemals einchecken!
# --- Datenbank ---------------------------------------------------------------
# Mindestens 8 Zeichen, Groß-/Kleinbuchstaben, Ziffern und Sonderzeichen (SQL-Server-Vorgabe).
SQL_SA_PASSWORD=Bitte-Aendern-2026!
# SQL-Server-Edition: Express (kostenlos, bis 10 GB) | Developer | Standard ...
MSSQL_PID=Express
# --- Initialer Administrator (nur beim allerersten Start) -----------------------
# Das Passwort muss beim ersten Login geändert werden.
ADMIN_USERNAME=admin
ADMIN_PASSWORD=Erstes-Passwort-2026
ADMIN_EMAIL=
FAMILY_NAME=Familie Muster
# --- Netzwerk -------------------------------------------------------------------
# Ohne Caddy: Port des Web-Containers auf dem Host (127.0.0.1 = nur lokal bzw. für eigenen Reverse Proxy).
WEB_BIND=127.0.0.1
WEB_PORT=8080
# Mit Caddy (docker compose --profile proxy up -d): öffentlicher Domainname für das TLS-Zertifikat.
DOMAIN=familie.example.org
# --- Images ----------------------------------------------------------------------
# Optional: Registry-Präfix inkl. abschließendem Slash, z. B. ghcr.io/meinname/
REGISTRY=
TAG=latest
+12
View File
@@ -0,0 +1,12 @@
# Reverse Proxy mit automatischem HTTPS (Let's Encrypt). Domain kommt aus der Umgebungsvariable DOMAIN.
{$DOMAIN} {
encode zstd gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
-Server
}
# WebSockets (SignalR) werden automatisch durchgereicht.
reverse_proxy web:8080
}
+124
View File
@@ -0,0 +1,124 @@
# Produktivbetrieb auf einem Linux-Docker-Host.
#
# cp .env.example .env # Werte anpassen!
# docker compose up -d --build
# docker compose --profile proxy up -d --build # inkl. Caddy (HTTPS mit Let's Encrypt)
#
# Die Anwendung verlangt HTTPS (Secure-Cookies). Ohne Caddy entweder einen eigenen Reverse Proxy
# mit TLS davor setzen oder nur über http://localhost testen.
name: familyplanner
x-app-build: &app-build
context: ..
dockerfile: Dockerfile
x-connection-string: &connection-string
ConnectionStrings__familyplannerdb: "Server=sql,1433;Database=familyplannerdb;User Id=sa;Password=${SQL_SA_PASSWORD};TrustServerCertificate=True;Encrypt=True"
services:
sql:
image: mcr.microsoft.com/mssql/server:2022-latest
# Das Image läuft als Benutzer "mssql" (non-root).
environment:
ACCEPT_EULA: "Y"
MSSQL_SA_PASSWORD: ${SQL_SA_PASSWORD:?SQL_SA_PASSWORD in .env setzen}
MSSQL_PID: ${MSSQL_PID:-Express}
TZ: Europe/Berlin
volumes:
- sql-data:/var/opt/mssql
- sql-backup:/var/opt/mssql/backup
healthcheck:
test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P \"$$MSSQL_SA_PASSWORD\" -C -b -Q 'SELECT 1' > /dev/null"]
interval: 10s
timeout: 5s
retries: 20
start_period: 30s
restart: unless-stopped
migrations:
image: ${REGISTRY:-}familyplanner-migrations:${TAG:-latest}
build:
<<: *app-build
args:
PROJECT: FamilyPlanner.MigrationService
environment:
<<: *connection-string
DOTNET_ENVIRONMENT: Production
Seed__AdminUserName: ${ADMIN_USERNAME:-admin}
Seed__AdminPassword: ${ADMIN_PASSWORD:?ADMIN_PASSWORD in .env setzen}
Seed__AdminEmail: ${ADMIN_EMAIL:-}
Seed__FamilyName: ${FAMILY_NAME:-}
depends_on:
sql:
condition: service_healthy
restart: "no"
api:
image: ${REGISTRY:-}familyplanner-api:${TAG:-latest}
build:
<<: *app-build
args:
PROJECT: FamilyPlanner.Api
environment:
<<: *connection-string
ASPNETCORE_ENVIRONMENT: Production
TZ: Europe/Berlin
depends_on:
migrations:
condition: service_completed_successfully
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
interval: 15s
timeout: 5s
retries: 5
start_period: 20s
restart: unless-stopped
web:
image: ${REGISTRY:-}familyplanner-web:${TAG:-latest}
build:
<<: *app-build
args:
PROJECT: FamilyPlanner.Web
environment:
ASPNETCORE_ENVIRONMENT: Production
TZ: Europe/Berlin
# Service Discovery: Adresse der Api im Docker-Netz (YARP-BFF)
services__api__http__0: http://api:8080
ports:
- "${WEB_BIND:-127.0.0.1}:${WEB_PORT:-8080}:8080"
depends_on:
api:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/alive"]
interval: 15s
timeout: 5s
retries: 5
start_period: 15s
restart: unless-stopped
caddy:
profiles: ["proxy"]
image: caddy:2
environment:
DOMAIN: ${DOMAIN:-localhost}
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
depends_on:
web:
condition: service_healthy
restart: unless-stopped
volumes:
sql-data:
sql-backup:
caddy-data:
caddy-config:
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env sh
# Erstellt ein Voll-Backup der Datenbank im Volume "sql-backup" und kopiert es zusätzlich nach ./backups.
# Aufruf aus dem Ordner deploy/: ./scripts/backup.sh
set -eu
cd "$(dirname "$0")/.."
. ./.env
STAMP=$(date +%Y%m%d-%H%M%S)
FILE="familyplanner-${STAMP}.bak"
docker compose exec -T sql /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P "$SQL_SA_PASSWORD" -C -b \
-Q "BACKUP DATABASE [familyplannerdb] TO DISK = N'/var/opt/mssql/backup/${FILE}' WITH COMPRESSION, CHECKSUM, INIT"
mkdir -p backups
docker compose cp "sql:/var/opt/mssql/backup/${FILE}" "backups/${FILE}"
echo "Backup erstellt: backups/${FILE}"
# Alte Backups im Volume aufräumen (älter als 14 Tage).
docker compose exec -T sql sh -c "find /var/opt/mssql/backup -name 'familyplanner-*.bak' -mtime +14 -delete"
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env sh
# Stellt die Datenbank aus einer Backup-Datei wieder her.
# Aufruf aus dem Ordner deploy/: ./scripts/restore.sh backups/familyplanner-20261009-120000.bak
set -eu
if [ $# -ne 1 ] || [ ! -f "$1" ]; then
echo "Aufruf: $0 <backup-datei.bak>" >&2
exit 1
fi
cd "$(dirname "$0")/.."
. ./.env
FILE=$(basename "$1")
echo "Stoppe Anwendung ..."
docker compose stop web api
docker compose cp "$1" "sql:/var/opt/mssql/backup/${FILE}"
docker compose exec -T sql /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P "$SQL_SA_PASSWORD" -C -b \
-Q "ALTER DATABASE [familyplannerdb] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; RESTORE DATABASE [familyplannerdb] FROM DISK = N'/var/opt/mssql/backup/${FILE}' WITH REPLACE, CHECKSUM; ALTER DATABASE [familyplannerdb] SET MULTI_USER;"
echo "Starte Anwendung ..."
docker compose up -d
echo "Wiederherstellung abgeschlossen."