diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..11a2eb0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,21 @@ +# Build-Ergebnisse und Abhängigkeiten kommen aus dem Image-Build, nicht vom Host. +backend/target/ +frontend/node_modules/ +frontend/dist/ +*.tsbuildinfo + +# Lokale Konfiguration +frontend/.env +frontend/.env.local + +# Tooling / OS / VCS +.git/ +.gitignore +.vscode/ +.idea/ +*.iml +.DS_Store +*.log +README.md +Dockerfile +.dockerignore diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..872c779 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,76 @@ +# syntax=docker/dockerfile:1 + +# SwyxWeb als ein Container: Das React-Frontend wird gebaut, in die statischen +# Ressourcen des Backends gelegt und zusammen mit ihm in ein Jar gepackt. +# Ausgeliefert wird am Ende alles über den einen Port des Backends (8080). +# +# Der eingebaute SwyxTray-Mock (/ws und /api/mock/**) läuft NICHT mit: seine +# Bohnen hängen am Spring-Profil "mock", und das setzt dieses Image nicht. +# +# Die beiden Build-Stufen laufen bewusst auf $BUILDPLATFORM, also nativ auf dem +# bauenden Rechner: JS-Bundle und Jar sind architekturunabhängig. Nur die +# Laufzeitstufe wird für die Zielarchitektur gezogen. Ein Build mit +# --platform linux/amd64 auf einem ARM-Mac läuft dadurch ohne Emulation. + +# --------------------------------------------------------------------------- +# 1. Frontend bauen (Vite -> frontend/dist) +# --------------------------------------------------------------------------- +FROM --platform=$BUILDPLATFORM node:22-alpine AS frontend +WORKDIR /build + +# Erst die Abhängigkeiten, damit diese Schicht im Cache bleibt, solange sich +# package.json/package-lock.json nicht ändern. +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci + +COPY frontend/ ./ +# VITE_WS_URL nur setzen, wenn die Adresse fest ins Bundle soll; im Normalfall +# kommt sie zur Laufzeit aus /api/config (siehe README). +ARG VITE_WS_URL +RUN npm run build + +# --------------------------------------------------------------------------- +# 2. Backend bauen (Spring Boot 4.1, Java 21) inkl. Frontend als static/ +# --------------------------------------------------------------------------- +FROM --platform=$BUILDPLATFORM maven:3.9-eclipse-temurin-21 AS backend +WORKDIR /build + +# Abhängigkeiten vorab auflösen – eigene Schicht, unabhängig vom Quelltext. +COPY backend/pom.xml ./ +RUN --mount=type=cache,target=/root/.m2 \ + mvn -B -q dependency:go-offline + +COPY backend/src ./src +COPY --from=frontend /build/dist/ ./src/main/resources/static/ + +RUN --mount=type=cache,target=/root/.m2 \ + mvn -B -DskipTests clean package \ + && cp target/*.jar /build/app.jar + +# --------------------------------------------------------------------------- +# 3. Laufzeit-Image +# --------------------------------------------------------------------------- +FROM eclipse-temurin:21-jre-alpine AS runtime + +# Nicht als root laufen. +RUN addgroup -S swyx && adduser -S -G swyx swyx +WORKDIR /app + +COPY --from=backend --chown=swyx:swyx /build/app.jar ./app.jar + +USER swyx +EXPOSE 8080 + +# Adresse der SwyxTray-App zur Laufzeit überschreibbar (Spring Relaxed Binding): +# -e APP_WEBSOCKET_HOST=192.168.180.135 -e APP_WEBSOCKET_PORT=17654 +# -e APP_WEBSOCKET_PATH=/ws -e APP_WEBSOCKET_SECURE=false +# +# SPRING_PROFILES_ACTIVE bleibt bewusst leer – der SwyxTray-Mock bliebe sonst +# beim Ausrollen erreichbar. Für eine Vorführung ohne Telefonanlage lässt er +# sich einzeln einschalten: -e SPRING_PROFILES_ACTIVE=mock +ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0" + +HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \ + CMD wget -q -O /dev/null http://127.0.0.1:8080/api/config || exit 1 + +ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"] diff --git a/docker_push.sh b/docker_push.sh new file mode 100755 index 0000000..da8ba5f --- /dev/null +++ b/docker_push.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash + +set -euo pipefail + +readonly SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +# Paket-Registry des Gitea auf gitea.appcreation.de; der Pfad ist dort +# //. +readonly REGISTRY_IMAGE="gitea.appcreation.de/appcreation/swyxweb" +readonly BACKEND_DIR="${SCRIPT_DIR}/backend" +readonly PLATFORM="linux/amd64" + +usage() { + cat <<'EOF' +Verwendung: + ./docker_push.sh [x.y.z] [--dry-run] + +Beispiel: + ./docker_push.sh 0.9.13 + ./docker_push.sh + ./docker_push.sh 0.9.13 --dry-run # nur bauen, nicht pushen + +Voraussetzungen: + - Docker Buildx ist installiert + - Login zur Registry wurde bereits ausgeführt: + docker login gitea.appcreation.de + +Ohne Versionsargument wird die Version aus backend/pom.xml verwendet. + +Gebaut wird das Dockerfile im Wurzelverzeichnis: Es übersetzt Frontend *und* +Backend selbst, ein vorheriger lokaler Maven- oder npm-Lauf ist also nicht +nötig. Der SwyxTray-Mock ist im Image nicht enthalten (Profil "mock"). +EOF +} + +fail() { + echo "Fehler: $*" >&2 + exit 1 +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || fail "'$1' wurde nicht gefunden." +} + +# Die Version steht in backend/pom.xml; ausgelesen wird sie mit dem Maven- +# Wrapper des Backends (backend/mvnw), nicht mit einem global installierten Maven. +resolve_pom_version() { + [[ -x "${BACKEND_DIR}/mvnw" ]] || fail "'${BACKEND_DIR}/mvnw' wurde nicht gefunden oder ist nicht ausführbar." + + local version + version="$( + cd "${BACKEND_DIR}" && ./mvnw -q -DforceStdout help:evaluate -Dexpression=project.version \ + | awk 'NF { last = $0 } END { print last }' + )" + + [[ -n "${version}" ]] || fail "Version konnte nicht aus der pom.xml ermittelt werden." + echo "${version}" +} + +VERSION="" +DRY_RUN=false + +for arg in "$@"; do + case "${arg}" in + -h|--help) + usage + exit 0 + ;; + --dry-run) + DRY_RUN=true + ;; + -*) + fail "Unbekannte Option '${arg}'." + ;; + *) + [[ -z "${VERSION}" ]] || fail "Es ist nur eine Versionsnummer erlaubt." + VERSION="${arg}" + ;; + esac +done + +[[ -n "${VERSION}" ]] || VERSION="$(resolve_pom_version)" + +# -SNAPSHOT ist zugelassen, weil die pom.xml zurzeit 0.0.1-SNAPSHOT trägt. +if [[ ! "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-SNAPSHOT)?$ ]]; then + fail "Versionsnummer muss das Format x.y.z oder x.y.z-SNAPSHOT haben (war: '${VERSION}')." +fi + +require_command docker +docker buildx version >/dev/null 2>&1 || fail "Docker Buildx ist nicht verfügbar." + +cd "${SCRIPT_DIR}" + +if [[ "${DRY_RUN}" == true ]]; then + echo "Baue Image ${REGISTRY_IMAGE}:${VERSION} für ${PLATFORM} (dry run, kein Push) ..." + docker buildx build \ + --platform "${PLATFORM}" \ + -f "${SCRIPT_DIR}/Dockerfile" \ + -t "${REGISTRY_IMAGE}:${VERSION}" \ + "${SCRIPT_DIR}" + echo "Fertig gebaut, nicht gepusht: ${REGISTRY_IMAGE}:${VERSION}" + exit 0 +fi + +echo "Baue und pushe Image ${REGISTRY_IMAGE}:${VERSION} für ${PLATFORM} ..." +docker buildx build \ + --platform "${PLATFORM}" \ + -f "${SCRIPT_DIR}/Dockerfile" \ + -t "${REGISTRY_IMAGE}:${VERSION}" \ + --push \ + "${SCRIPT_DIR}" + +echo "Fertig: ${REGISTRY_IMAGE}:${VERSION}"