From a961a13be21551d25ba299f065a0c9d72a66a486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Reinhard=20X=2E=20F=C3=BCrst?= Date: Sun, 21 Jun 2026 17:29:39 +0200 Subject: [PATCH] Angepass an Produtionumgebung --- .env.example | 19 ++++++++ backend/Dockerfile.prod | 17 +++++++ backend/src/server.js | 29 +++++++----- deploy.sh | 96 +++++++++++++++++++++++++++++++++++++++ docker-compose.images.yml | 65 ++++++++++++++++++++++++++ docker-compose.prod.yml | 60 ++++++++++++++++++++++++ docker-compose.yml | 3 ++ frontend/Dockerfile.prod | 23 ++++++++++ frontend/eslint.config.js | 7 +++ frontend/nginx.conf | 23 ++++++++++ frontend/src/App.jsx | 8 +++- frontend/vite.config.js | 13 ++++++ 12 files changed, 349 insertions(+), 14 deletions(-) create mode 100644 .env.example create mode 100644 backend/Dockerfile.prod create mode 100755 deploy.sh create mode 100644 docker-compose.images.yml create mode 100644 docker-compose.prod.yml create mode 100644 frontend/Dockerfile.prod create mode 100644 frontend/nginx.conf diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ab14de9 --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +# Vorlage für die Produktionsumgebung. +# Kopieren nach .env und Werte anpassen: cp .env.example .env + +# --- MongoDB Zugangsdaten (werden beim ersten Start der DB angelegt) --- +MONGO_ROOT_USER=root +MONGO_ROOT_PASSWD=bitte-aendern + +# --- Frontend --- +# Host-Port, unter dem die App erreichbar ist (Standard: 80) +FRONTEND_PORT=80 + +# --- Images (nur für docker-compose.images.yml) --- +# Tag der Images aus docker.citysensor.de (Standard: latest) +# IMAGE_TAG=latest + +# --- Backend / CORS --- +# Nur nötig, falls das Frontend NICHT über denselben Host/nginx-Proxy läuft. +# Standardmäßig nicht erforderlich. Mehrere Origins kommagetrennt, "*" für alle. +# CORS_ORIGIN=https://meine-domain.de diff --git a/backend/Dockerfile.prod b/backend/Dockerfile.prod new file mode 100644 index 0000000..6025ab7 --- /dev/null +++ b/backend/Dockerfile.prod @@ -0,0 +1,17 @@ +FROM node:20-alpine + +WORKDIR /app + +# Nur Produktions-Abhängigkeiten installieren +COPY package*.json ./ +RUN npm ci --omit=dev + +# Quellcode kopieren +COPY . . + +ENV NODE_ENV=production + +EXPOSE 3001 + +# Produktionsstart (ohne nodemon) +CMD ["npm", "start"] diff --git a/backend/src/server.js b/backend/src/server.js index 61b8c50..2ca629b 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -9,22 +9,27 @@ import { ObjectId } from 'mongodb'; // Wichtig für die Arbeit mit MongoDB IDs const app = express(); const PORT = process.env.PORT || 3001; -// Middleware konfigurieren - CORS muss vor allen Routen kommen +// Erlaubte CORS-Origins aus der Umgebung (kommagetrennt). +// In Produktion wird das Frontend i.d.R. über denselben Host (nginx-Proxy) +// ausgeliefert, dann sind keine Cross-Origin-Anfragen nötig. +// Standard: localhost:5173 für die lokale Entwicklung. +// CORS_ORIGIN="*" erlaubt alle Origins. +const corsOrigins = (process.env.CORS_ORIGIN || 'http://localhost:5173') + .split(',') + .map((o) => o.trim()); + +app.use(cors({ + origin: corsOrigins.includes('*') ? true : corsOrigins, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Origin', 'X-Requested-With', 'Content-Type', 'Accept'], + credentials: true, +})); + app.use((req, res, next) => { console.log(`📥 ${req.method} ${req.path} from ${req.get('origin') || 'unknown origin'}`); - res.header('Access-Control-Allow-Origin', 'http://localhost:5173'); - res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); - res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); - res.header('Access-Control-Allow-Credentials', 'true'); - - // Handle preflight requests - if (req.method === 'OPTIONS') { - console.log('✅ Preflight request handled'); - return res.sendStatus(200); - } next(); }); -app.use(express.json()); +app.use(express.json()); // ----------------------------------------------------- // API ROUTEN diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..891ce37 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +# Deploy Script +# Baut das Docker Image und lädt es zur Registry hoch + +set -e + +# Konfiguration +REGISTRY="docker.citysensor.de" +PROJEKT="wetterstation" +IMAGE_NAME=("${PROJEKT}-frontend" "${PROJEKT}-backend") +TAG="${TAG:-$(date +%Y%m%d%H%M)}" # default Datum + +# Build-Datum und Version +BUILD_DATE=$(date +%d.%m.%Y) +VERSION=$(grep '"version"' frontend/package.json | head -1 | sed 's/.*"version": "\(.*\)".*/\1/') + +echo "==========================================" +echo " Deploy Script" +echo "==========================================" +echo "Registry: ${REGISTRY}" +echo "Images: ${IMAGE_NAME[*]}" +echo "Tag: ${TAG}" +echo "Build-Datum: ${BUILD_DATE}" +echo "==========================================" +echo "" + +# 1. Login zur Registry (falls noch nicht eingeloggt) +echo ">>> Login zu ${REGISTRY}..." +docker login "${REGISTRY}" +echo "" + +# 2. Multiplatform Builder einrichten (docker-container driver erforderlich) +echo ">>> Richte Multiplatform Builder ein..." +if ! docker buildx inspect multiplatform-builder &>/dev/null; then + docker buildx create --name multiplatform-builder --driver docker-container --bootstrap +fi +docker buildx use multiplatform-builder +echo "" + +for image in "${IMAGE_NAME[@]}"; do + # Entferne Projekt-Präfix für Verzeichnisnamen + IMAGE_DIR="${image#${PROJEKT}-}" + FULL_IMAGE="${REGISTRY}/${image}:${TAG}" + + echo "==========================================" + echo ">>> Baue ${image}..." + echo ">>> Image: ${FULL_IMAGE}" + echo "==========================================" + + # Build-Args vorbereiten (für Frontend Version und Build-Date) + BUILD_ARGS="--build-arg BUILD_DATE=${BUILD_DATE} --build-arg VERSION=${VERSION}" + + # 3. Docker Image bauen und pushen (Multiplatform) + # monitor: Build-Kontext ist Projekt-Root (check_wetterserver.py liegt dort) + if [[ "${image}" == "${PROJEKT}-monitor" ]]; then + DOCKERFILE_ARG="-f monitor/Dockerfile" + BUILD_CONTEXT="." + else + DOCKERFILE_ARG="" + BUILD_CONTEXT="./${IMAGE_DIR}" + fi + + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + ${BUILD_ARGS} \ + ${DOCKERFILE_ARG} \ + -t "${FULL_IMAGE}" \ + --push \ + "${BUILD_CONTEXT}" + + # 4. Tagge auch als :${VERSION} und :latest + echo ">>> Tagge ${image} als :${VERSION} und :latest..." + docker buildx imagetools create \ + -t "${REGISTRY}/${image}:${VERSION}" \ + -t "${REGISTRY}/${image}:latest" \ + "${FULL_IMAGE}" + + echo "✓ ${image} erfolgreich gebaut und gepusht!" + echo "" +done + +echo ">>> Alle Builds erfolgreich!" + +echo "" +echo "==========================================" +echo "✓ Deploy erfolgreich abgeschlossen!" +echo "==========================================" +echo "Registry: ${REGISTRY}" +echo "Projekt: ${PROJEKT}" +echo "Tag: ${TAG}" +echo "" +echo "Auf dem Server ausführen:" +echo " docker compose -f docker-compose.prod.yml pull" +echo " docker compose -f docker-compose.prod.yml up -d" +echo "" diff --git a/docker-compose.images.yml b/docker-compose.images.yml new file mode 100644 index 0000000..b3286fc --- /dev/null +++ b/docker-compose.images.yml @@ -0,0 +1,65 @@ +# Produktions-Setup mit vorgebauten Images aus der Registry docker.citysensor.de. +# Geeignet für Stack-Manager wie dockhand/Dockge, wo nur die Compose-Datei +# im Stack-Verzeichnis liegt. +# +# 1) Images bauen und in die Registry pushen (im Projektverzeichnis mit Quellcode): +# docker build -t docker.citysensor.de/aerzte-backend:latest -f ./backend/Dockerfile.prod ./backend +# docker build -t docker.citysensor.de/aerzte-frontend:latest -f ./frontend/Dockerfile.prod ./frontend +# docker push docker.citysensor.de/aerzte-backend:latest +# docker push docker.citysensor.de/aerzte-frontend:latest +# +# 2) Diese Datei (+ .env) ins Stack-Verzeichnis legen und starten: +# docker compose pull && docker compose up -d +# +# Benötigt eine .env-Datei (siehe .env.example) mit: +# MONGO_ROOT_USER, MONGO_ROOT_PASSWD und optional FRONTEND_PORT, IMAGE_TAG. + +services: + mongodb: + image: mongo:latest + container_name: mongodb + restart: unless-stopped + # Kein Host-Port: MongoDB ist nur im internen Docker-Netz erreichbar + environment: + MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USER} + MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWD} + volumes: + - mongodb_data:/data/db + - mongodb_config:/data/configdb + networks: + - app-network + + backend: + image: docker.citysensor.de/aerzte-backend:${IMAGE_TAG:-latest} + container_name: backend + restart: unless-stopped + # Kein Host-Port: Das Backend wird nur intern über nginx (Frontend) angesprochen + environment: + - PORT=3001 + - NODE_ENV=production + - MONGO_URI=mongodb://${MONGO_ROOT_USER}:${MONGO_ROOT_PASSWD}@mongodb:27017/appointmentsdb?authSource=admin + # Frontend läuft über denselben Host (nginx-Proxy) -> kein Cross-Origin nötig. + - CORS_ORIGIN=${CORS_ORIGIN:-http://localhost:5173} + depends_on: + - mongodb + networks: + - app-network + + frontend: + image: docker.citysensor.de/aerzte-frontend:${IMAGE_TAG:-latest} + container_name: frontend + restart: unless-stopped + ports: + - "${FRONTEND_PORT:-80}:80" + depends_on: + - backend + networks: + - app-network + +volumes: + mongodb_data: + mongodb_config: + +networks: + app-network: + driver: bridge diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..411898d --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,60 @@ +# Produktions-Setup für den Serverbetrieb. +# Starten mit: docker compose -f docker-compose.prod.yml up -d --build +# +# Benötigt eine .env-Datei (siehe .env.example) mit: +# MONGO_ROOT_USER, MONGO_ROOT_PASSWD und optional FRONTEND_PORT. + +services: + mongodb: + image: mongo:latest + container_name: mongodb + restart: unless-stopped + # Kein Host-Port: MongoDB ist nur im internen Docker-Netz erreichbar + environment: + MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USER} + MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWD} + volumes: + - mongodb_data:/data/db + - mongodb_config:/data/configdb + networks: + - app-network + + backend: + build: + context: ./backend + dockerfile: Dockerfile.prod + container_name: backend + restart: unless-stopped + # Kein Host-Port: Das Backend wird nur intern über nginx (Frontend) angesprochen + environment: + - PORT=3001 + - NODE_ENV=production + - MONGO_URI=mongodb://${MONGO_ROOT_USER}:${MONGO_ROOT_PASSWD}@mongodb:27017/appointmentsdb?authSource=admin + # Frontend läuft über denselben Host (nginx-Proxy) -> kein Cross-Origin nötig. + # Bei Bedarf hier die öffentliche Frontend-URL eintragen. + - CORS_ORIGIN=${CORS_ORIGIN:-http://localhost:5173} + depends_on: + - mongodb + networks: + - app-network + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile.prod + container_name: frontend + restart: unless-stopped + ports: + - "${FRONTEND_PORT:-80}:80" + depends_on: + - backend + networks: + - app-network + +volumes: + mongodb_data: + mongodb_config: + +networks: + app-network: + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index 70626db..5dca7d4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,6 +42,9 @@ services: restart: unless-stopped ports: - "5173:5173" + environment: + # Vite proxyt /api an das Backend im selben Compose-Netz + - VITE_PROXY_TARGET=http://backend:3001 depends_on: - backend volumes: diff --git a/frontend/Dockerfile.prod b/frontend/Dockerfile.prod new file mode 100644 index 0000000..26bcb13 --- /dev/null +++ b/frontend/Dockerfile.prod @@ -0,0 +1,23 @@ +# --- Build-Stage: Statische Dateien mit Vite bauen --- +FROM node:20-alpine AS build + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . +RUN npm run build + +# --- Serve-Stage: Auslieferung über nginx --- +FROM nginx:alpine + +# Eigene nginx-Konfiguration (statische Auslieferung + /api-Proxy) +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Gebaute Dateien aus der Build-Stage übernehmen +COPY --from=build /app/dist /usr/share/nginx/html + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 4fa125d..4fd68c5 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -26,4 +26,11 @@ export default defineConfig([ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], }, }, + { + // Konfigurationsdateien laufen in Node, nicht im Browser + files: ['*.config.js'], + languageOptions: { + globals: globals.node, + }, + }, ]) diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..65c3f69 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,23 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # API-Anfragen an das Backend weiterleiten. + # "backend" ist der Service-Name aus docker-compose. + location /api/ { + proxy_pass http://backend:3001; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # SPA-Routing: alle übrigen Pfade auf index.html (React Router etc.) + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 6ff2c7e..b8999ff 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -5,8 +5,12 @@ import ConfirmationModal from './components/ConfirmationModal'; import './AppStyles.css'; // Die Basis-URL der Express-API -// Always use localhost because the browser runs on the host machine -const API_URL = 'http://localhost:3001/api/appointments'; +// Standardmäßig relativ ("/api/appointments"), damit der Browser denselben +// Host wie das Frontend anspricht. In Produktion leitet nginx /api an das +// Backend weiter, im Dev-Modus übernimmt das der Vite-Proxy (vite.config.js). +// Über VITE_API_URL kann optional ein absoluter Backend-Host gesetzt werden. +const API_BASE = import.meta.env.VITE_API_URL ?? ''; +const API_URL = `${API_BASE}/api/appointments`; // Funktion zur Umwandlung von MongoDBs _id in die interne id des Frontends const normalizeAppointment = (appointment) => ({ diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 7bf5433..c9856f2 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,6 +1,13 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +// Im Dev-Modus laufen Frontend (5173) und Backend (3001) getrennt. +// Damit das Frontend trotzdem relative URLs ("/api/...") verwenden kann, +// proxyt Vite alle /api-Anfragen an das Backend. Das Ziel ist konfigurierbar: +// - lokal ohne Docker: http://localhost:3001 (Standard) +// - Docker-Dev: VITE_PROXY_TARGET=http://backend:3001 +const proxyTarget = process.env.VITE_PROXY_TARGET || 'http://localhost:3001' + // https://vite.dev/config/ export default defineConfig({ plugins: [react()], @@ -9,6 +16,12 @@ export default defineConfig({ port: 5173, watch: { usePolling: true + }, + proxy: { + '/api': { + target: proxyTarget, + changeOrigin: true + } } } })