diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..604a323 --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# Rezepte Environment Example + +# --- Domain & Routing --- +DOMAIN=example.com +ACME_EMAIL=admin@example.com + +# --- Database --- +MYSQL_PASSWORD=change_this_password +MYSQL_ROOT_PASSWORD=change_this_root_password + +# --- Auth / Security --- +JWT_SECRET=please_change_to_secure_32_char_min +CORS_ORIGIN=https://rezepte.${DOMAIN} + +# --- Images from Registry (override if pushing to GHCR or custom registry) --- +BACKEND_IMAGE=docker.citysensor.de/rezepte-backend:latest +FRONTEND_IMAGE=docker.citysensor.de/rezepte-frontend:latest + +# --- Frontend API URL (if building locally outside compose) --- +PRODUCTION_API_URL=https://rezepte.${DOMAIN}/api + +# --- Optional HOST_IP for local network override builds --- +HOST_IP=192.168.1.100 diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index f57845d..ee926a8 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -11,6 +11,8 @@ env: REGISTRY: ${{ vars.DOCKER_REGISTRY || 'docker.citysensor.de' }} IMAGE_NAME_BACKEND: rezepte-backend IMAGE_NAME_FRONTEND: rezepte-frontend + DATE_TAG: ${{ github.run_id }}-${{ github.run_number }} + jobs: build-and-push: @@ -47,6 +49,8 @@ jobs: type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=raw,value=latest,enable={{is_default_branch}} + type=sha + type=raw,value=${{ env.DATE_TAG }} - name: Extract metadata for frontend id: meta-frontend @@ -59,6 +63,8 @@ jobs: type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=raw,value=latest,enable={{is_default_branch}} + type=sha + type=raw,value=${{ env.DATE_TAG }} - name: Build and push backend image uses: docker/build-push-action@v5 @@ -67,36 +73,40 @@ jobs: push: true tags: ${{ steps.meta-backend.outputs.tags }} labels: ${{ steps.meta-backend.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max - name: Build and push frontend image uses: docker/build-push-action@v5 with: context: ./frontend build-args: | - VITE_API_BASE_URL=${{ secrets.PRODUCTION_API_URL || 'https://yourdomain.com/api' }} + VITE_API_URL=${{ secrets.PRODUCTION_API_URL || 'https://yourdomain.com/api' }} push: true tags: ${{ steps.meta-frontend.outputs.tags }} labels: ${{ steps.meta-frontend.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max - name: Create deployment summary run: | echo "## 🚀 Deployment Images Built" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "### Backend Image" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BACKEND }}:latest" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "### Backend Image Tags" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "${{ steps.meta-backend.outputs.tags }}" | tr ' ' '\n' >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "### Frontend Image" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "${{ env.REGISTRY }}/${{ env.IMAGE_NAME_FRONTEND }}:latest" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "### Frontend Image Tags" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "${{ steps.meta-frontend.outputs.tags }}" | tr ' ' '\n' >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### 📋 Server Deployment" >> $GITHUB_STEP_SUMMARY echo "Update your server's \`.env.production\` with:" >> $GITHUB_STEP_SUMMARY echo "\`\`\`env" >> $GITHUB_STEP_SUMMARY - echo "BACKEND_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BACKEND }}:latest" >> $GITHUB_STEP_SUMMARY - echo "FRONTEND_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_FRONTEND }}:latest" >> $GITHUB_STEP_SUMMARY + echo "BACKEND_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BACKEND }}:@sha||latest" >> $GITHUB_STEP_SUMMARY + echo "FRONTEND_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_FRONTEND }}:@sha||latest" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Then run: \`./deploy-registry.sh\`" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/README.md b/README.md index ffcbdeb..2cbb3e0 100644 --- a/README.md +++ b/README.md @@ -119,4 +119,7 @@ Beim ersten Start importiert MySQL automatisch SQL-Skripte aus `sql-init/`. ## Weiterführend - Moderne Stack Doku: `NODEJS_README.md` - Traefik / Registry Deploy: siehe entsprechende `*_SETUP.md` Dateien -- Sicherheitshärtung TODO: CORS einschränken, CSP Header, chown Migration verbessern \ No newline at end of file +- Sicherheitshärtung TODO: CORS einschränken, CSP Header, chown Migration verbessern + - Erweiterte Compose Overrides: siehe `legacy/README_COMPOSE_LEGACY.md` + - Beispiel Umgebungsvariablen: `.env.example` + - CI Build Tags: Branch, Semver, `sha-`, Run-ID, `latest` (nur main) \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..caf88ab --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,71 @@ +services: + mysql: + # Production DB (no host port exposure by default) + image: mysql:8.0 + container_name: rezepte-mysql-prod + restart: unless-stopped + environment: + MYSQL_DATABASE: rezepte + MYSQL_USER: rezepte_user + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-change_this_password} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-change_this_root_password} + volumes: + - mysql_data:/var/lib/mysql + # Optional initial import only for first deploy (remove afterwards) + # - ./Rezepte.sql:/docker-entrypoint-initdb.d/01-rezepte.sql:ro + # - ./ingredients.sql:/docker-entrypoint-initdb.d/02-ingredients.sql:ro + # - ./Zubereitung.sql:/docker-entrypoint-initdb.d/03-zubereitung.sql:ro + # - ./rezepte_bilder.sql:/docker-entrypoint-initdb.d/04-bilder.sql:ro + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + timeout: 20s + retries: 10 + networks: + - rezepte-network + + backend: + image: ${BACKEND_IMAGE:-ghcr.io/your-username/rezepte-backend:latest} + container_name: rezepte-backend-prod + restart: unless-stopped + environment: + NODE_ENV: production + DATABASE_URL: mysql://rezepte_user:${MYSQL_PASSWORD:-change_this_password}@mysql:3306/rezepte + JWT_SECRET: ${JWT_SECRET:-change_this_jwt_secret_min_32_characters} + CORS_ORIGIN: ${CORS_ORIGIN:-https://rezepte.${DOMAIN}} + PORT: 3001 + volumes: + - uploads_data:/app/uploads + depends_on: + mysql: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3001/api/health"] + interval: 30s + timeout: 10s + retries: 3 + networks: + - rezepte-network + + frontend: + image: ${FRONTEND_IMAGE:-ghcr.io/your-username/rezepte-frontend:latest} + container_name: rezepte-frontend-prod + restart: unless-stopped + depends_on: + - backend + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/"] + interval: 30s + timeout: 10s + retries: 3 + networks: + - rezepte-network + +volumes: + mysql_data: + driver: local + uploads_data: + driver: local + +networks: + rezepte-network: + driver: bridge diff --git a/docker-compose.registry.override.yml b/docker-compose.registry.override.yml new file mode 100644 index 0000000..ad99358 --- /dev/null +++ b/docker-compose.registry.override.yml @@ -0,0 +1,7 @@ +services: + backend: + image: ${BACKEND_IMAGE:-ghcr.io/your-username/rezepte-backend:latest} + build: null + frontend: + image: ${FRONTEND_IMAGE:-ghcr.io/your-username/rezepte-frontend:latest} + build: null diff --git a/docker-compose.traefik.override.yml b/docker-compose.traefik.override.yml new file mode 100644 index 0000000..d367da1 --- /dev/null +++ b/docker-compose.traefik.override.yml @@ -0,0 +1,79 @@ +services: + traefik: + image: traefik:v3.0 + container_name: traefik + restart: unless-stopped + command: + - --api.dashboard=true + - --api.insecure=false + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --providers.docker=true + - --providers.docker.exposedbydefault=false + - --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL} + - --certificatesresolvers.letsencrypt.acme.storage=/acme.json + - --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web + - --log.level=INFO + - --accesslog=true + - --entrypoints.web.http.redirections.entrypoint.to=websecure + - --entrypoints.web.http.redirections.entrypoint.scheme=https + - --entrypoints.web.http.redirections.entrypoint.permanent=true + ports: + - "80:80" + - "443:443" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - traefik_acme:/acme.json + labels: + - "traefik.enable=true" + - "traefik.http.routers.traefik.rule=Host(`traefik.${DOMAIN}`)" + - "traefik.http.routers.traefik.entrypoints=websecure" + - "traefik.http.routers.traefik.tls.certresolver=letsencrypt" + - "traefik.http.routers.traefik.service=api@internal" + - "traefik.http.routers.traefik.middlewares=auth" + # Basic Auth Beispiel (unbedingt Hash anpassen) + - "traefik.http.middlewares.auth.basicauth.users=admin:$$2y$$10$$8eO9J8Ef.LswB5K4l1.ZJ.qZBOa6ZXJ3X2y3zCZLCr9zHVJ8vJ2Ga" + networks: + - traefik-network + - rezepte-network + + backend: + labels: + - "traefik.enable=true" + - "traefik.http.routers.backend.rule=Host(`rezepte.${DOMAIN}`) && PathPrefix(`/api`)" + - "traefik.http.routers.backend.entrypoints=websecure" + - "traefik.http.routers.backend.tls.certresolver=letsencrypt" + - "traefik.http.services.backend.loadbalancer.server.port=3001" + - "traefik.http.routers.backend-uploads.rule=Host(`rezepte.${DOMAIN}`) && PathPrefix(`/uploads`)" + - "traefik.http.routers.backend-uploads.entrypoints=websecure" + - "traefik.http.routers.backend-uploads.tls.certresolver=letsencrypt" + - "traefik.http.routers.backend-uploads.service=backend" + - "traefik.http.routers.backend.priority=10" + - "traefik.http.routers.backend-uploads.priority=10" + + frontend: + labels: + - "traefik.enable=true" + - "traefik.http.routers.frontend.rule=Host(`rezepte.${DOMAIN}`)" + - "traefik.http.routers.frontend.entrypoints=websecure" + - "traefik.http.routers.frontend.tls.certresolver=letsencrypt" + - "traefik.http.services.frontend.loadbalancer.server.port=80" + - "traefik.http.routers.frontend.priority=1" + + phpmyadmin: + labels: + - "traefik.enable=true" + - "traefik.http.routers.phpmyadmin.rule=Host(`phpmyadmin.${DOMAIN}`)" + - "traefik.http.routers.phpmyadmin.entrypoints=websecure" + - "traefik.http.routers.phpmyadmin.tls.certresolver=letsencrypt" + - "traefik.http.services.phpmyadmin.loadbalancer.server.port=80" + +volumes: + traefik_acme: + driver: local + +networks: + traefik-network: + driver: bridge + rezepte-network: + external: false diff --git a/legacy/README_COMPOSE_LEGACY.md b/legacy/README_COMPOSE_LEGACY.md new file mode 100644 index 0000000..60fe2b3 --- /dev/null +++ b/legacy/README_COMPOSE_LEGACY.md @@ -0,0 +1,51 @@ +# Archivierte Compose Varianten + +Die früheren Dateien `docker-compose.production.yml`, `docker-compose.registry.yml`, `docker-compose.local-network.yml`, `docker-compose.traefik.yml` wurden konsolidiert. + +## Neuer Ansatz + +Basis: `docker-compose.yml` + +Optionale Overrides: +- Produktion (ohne Traefik Build-Free): `docker-compose.prod.yml` +- Registry Images statt Build: `docker-compose.registry.override.yml` +- Traefik + HTTPS Routing: `docker-compose.traefik.override.yml` + +## Beispiele + +Lokale Entwicklung (modern stack): +```bash +docker compose up -d +``` + +Produktion (mit Registry Images): +```bash +docker compose -f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.registry.override.yml up -d +``` + +Produktion + Traefik (HTTPS / Domain Routing): +```bash +docker compose -f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.registry.override.yml -f docker-compose.traefik.override.yml up -d +``` + +Nur Traefik Layer nachträglich hinzufügen: +```bash +docker compose -f docker-compose.traefik.override.yml up -d traefik +``` + +## Variablen (.env empfohlen) +``` +DOMAIN=example.com +ACME_EMAIL=admin@example.com +MYSQL_PASSWORD=secure_db_pwd +MYSQL_ROOT_PASSWORD=secure_root_pwd +JWT_SECRET=32+_chars_secret_here +CORS_ORIGIN=https://rezepte.${DOMAIN} +BACKEND_IMAGE=ghcr.io//rezepte-backend:latest +FRONTEND_IMAGE=ghcr.io//rezepte-frontend:latest +``` + +## Hinweise +- Seed-SQL Mounts in Produktion nur beim Erst-Deploy aktivieren. +- Traefik Basic Auth Hash austauschen. +- `CORS_ORIGIN="*"` im Development nicht in Produktion übernehmen.