From a255543da6376eaec9bb3e6f1aa453d7984827f0 Mon Sep 17 00:00:00 2001 From: rxf Date: Mon, 22 Sep 2025 16:35:59 +0200 Subject: [PATCH] Docker mit traefik und portainer --- .DS_Store | Bin 6148 -> 6148 bytes .dockerignore | 43 ++- .env | 46 +++ .env.build.example | 23 ++ .env.development | 27 ++ .env.docker | 46 +++ .env.external-db.example | 40 +++ .env.local-network | 24 ++ .env.production.example | 18 ++ .env.registry.example | 30 ++ .env.traefik.example | 28 ++ .github/workflows/docker-build.yml | 96 ++++++ CITYSENSOR_SETUP.md | 179 +++++++++++ DEPLOYMENT.md | 301 ++++++++++++++++++ DOCKER_REGISTRY.md | 133 ++++++++ DOCKER_SETUP.md | 196 ++++++++++++ EXTERNAL_MYSQL_SETUP.md | 247 ++++++++++++++ PHPMYADMIN_SETUP.md | 156 +++++++++ PORTAINER_TRAEFIK_SETUP.md | 183 +++++++++++ SERVER_DEPLOYMENT_PACKAGE.md | 67 ++++ TRAEFIK_DEPLOYMENT.md | 177 ++++++++++ backup.sh | 69 ++++ build-and-push.sh | 83 +++++ deploy-external-db.sh | 166 ++++++++++ deploy-production.sh | 54 ++++ deploy-registry.sh | 74 +++++ deploy-traefik.sh | 91 ++++++ docker-compose.local-network.yml | 103 ++++++ docker-compose.modern.yml | 124 ++++++++ docker-compose.production.yml | 88 +++++ docker-compose.registry.yml | 80 +++++ docker-compose.traefik-external-db.yml | 175 ++++++++++ docker-compose.traefik.yml | 202 ++++++++++++ docker-deploy.sh | 84 +++++ docker-stop.sh | 22 ++ generate-jwt-secret.sh | 43 +++ nginx-rezepte-klaus.conf | 137 ++++++++ nodejs-version/backend/Dockerfile | 89 ++++++ nodejs-version/backend/dist/app.d.ts.map | 2 +- nodejs-version/backend/dist/app.js | 12 +- nodejs-version/backend/dist/app.js.map | 2 +- .../backend/dist/routes/images.d.ts.map | 2 +- nodejs-version/backend/dist/routes/images.js | 130 ++++++++ .../backend/dist/routes/images.js.map | 2 +- .../backend/dist/routes/recipes.d.ts.map | 2 +- nodejs-version/backend/dist/routes/recipes.js | 9 +- .../backend/dist/routes/recipes.js.map | 2 +- nodejs-version/backend/package-lock.json | 2 +- nodejs-version/backend/package.json | 2 +- nodejs-version/backend/src/app.ts | 36 ++- nodejs-version/backend/src/routes/images.ts | 183 ++++++++++- nodejs-version/backend/src/routes/recipes.ts | 10 +- nodejs-version/frontend/Dockerfile | 138 ++++++++ .../frontend/src/components/FileUpload.css | 296 +++++++++++++++++ .../frontend/src/components/FileUpload.tsx | 233 ++++++++++++++ .../frontend/src/components/RecipeCreate.tsx | 45 ++- .../frontend/src/components/RecipeDetail.css | 150 +++++++++ .../frontend/src/components/RecipeDetail.tsx | 105 ++++++ .../frontend/src/components/RecipeEdit.css | 31 ++ nodejs-version/frontend/src/services/api.ts | 44 ++- setup-citysensor.sh | 68 ++++ setup-dev.sh | 93 ++++++ start-local-network.sh | 103 ++++++ uploads/.DS_Store | Bin 6148 -> 6148 bytes 64 files changed, 5421 insertions(+), 25 deletions(-) create mode 100644 .env create mode 100644 .env.build.example create mode 100644 .env.development create mode 100644 .env.docker create mode 100644 .env.external-db.example create mode 100644 .env.local-network create mode 100644 .env.production.example create mode 100644 .env.registry.example create mode 100644 .env.traefik.example create mode 100644 .github/workflows/docker-build.yml create mode 100644 CITYSENSOR_SETUP.md create mode 100644 DEPLOYMENT.md create mode 100644 DOCKER_REGISTRY.md create mode 100644 DOCKER_SETUP.md create mode 100644 EXTERNAL_MYSQL_SETUP.md create mode 100644 PHPMYADMIN_SETUP.md create mode 100644 PORTAINER_TRAEFIK_SETUP.md create mode 100644 SERVER_DEPLOYMENT_PACKAGE.md create mode 100644 TRAEFIK_DEPLOYMENT.md create mode 100755 backup.sh create mode 100755 build-and-push.sh create mode 100755 deploy-external-db.sh create mode 100755 deploy-production.sh create mode 100755 deploy-registry.sh create mode 100755 deploy-traefik.sh create mode 100644 docker-compose.local-network.yml create mode 100644 docker-compose.modern.yml create mode 100644 docker-compose.production.yml create mode 100644 docker-compose.registry.yml create mode 100644 docker-compose.traefik-external-db.yml create mode 100644 docker-compose.traefik.yml create mode 100755 docker-deploy.sh create mode 100755 docker-stop.sh create mode 100755 generate-jwt-secret.sh create mode 100644 nginx-rezepte-klaus.conf create mode 100644 nodejs-version/backend/Dockerfile create mode 100644 nodejs-version/frontend/Dockerfile create mode 100644 nodejs-version/frontend/src/components/FileUpload.css create mode 100644 nodejs-version/frontend/src/components/FileUpload.tsx create mode 100755 setup-citysensor.sh create mode 100755 setup-dev.sh create mode 100755 start-local-network.sh diff --git a/.DS_Store b/.DS_Store index 204d1c51784fcd41f145fadb9d1e8c8f7eb34625..365cc4bd6be3e53112758c84edc8b65a6d314571 100644 GIT binary patch delta 72 zcmZoMXfc=|&e%4wP;8=}q9`K+0|O8XFfb%Cq%ahx6es5-> $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 "" >> $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 "" >> $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 "\`\`\`" >> $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/CITYSENSOR_SETUP.md b/CITYSENSOR_SETUP.md new file mode 100644 index 0000000..6601250 --- /dev/null +++ b/CITYSENSOR_SETUP.md @@ -0,0 +1,179 @@ +# CitySensor Docker Registry Integration + +## 🏢 Überblick + +Diese Konfiguration ist speziell für die **CitySensor Docker Registry** (`docker.citysensor.de`) angepasst und unterstützt: + +- ✅ **Private Registry Authentication** mit Username/Passwort +- ✅ **Automatisches Login** in Build- und Deployment-Skripten +- ✅ **Traefik-Integration** für `rezepte.your.domain.com` +- ✅ **CI/CD Pipeline** mit GitHub Actions +- ✅ **Minimaler Server-Footprint** (~60 KB statt Repository-Clone) + +## 🚀 Schnell-Setup + +### 1. Build-Konfiguration erstellen: +```bash +cp .env.build.example .env.registry +# Edit .env.registry mit Ihren CitySensor-Zugangsdaten +``` + +### 2. Images bauen und pushen: +```bash +./build-and-push.sh +``` + +### 3. Server-Deployment: +```bash +# Dateien auf Server kopieren: +scp docker-compose.traefik.yml user@server:/opt/rezepte/ +scp .env.production user@server:/opt/rezepte/ +scp *.sql user@server:/opt/rezepte/ +scp deploy-traefik.sh user@server:/opt/rezepte/ + +# Auf Server deployen: +ssh user@server +cd /opt/rezepte +./deploy-traefik.sh +``` + +## 📁 Konfigurationsdateien + +### `.env.registry` (für Build): +```env +DOMAIN=example.com +API_BASE_URL=https://rezepte.example.com/api +DOCKER_REGISTRY=docker.citysensor.de +DOCKER_USERNAME=your_username +DOCKER_PASSWORD=your_password +MYSQL_PASSWORD=secure_db_password +MYSQL_ROOT_PASSWORD=super_secure_root_password +``` + +### `.env.production` (für Server): +```env +DOMAIN=example.com +ACME_EMAIL=admin@example.com +MYSQL_PASSWORD=secure_db_password +MYSQL_ROOT_PASSWORD=super_secure_root_password +DOCKER_REGISTRY=docker.citysensor.de +DOCKER_USERNAME=your_username +DOCKER_PASSWORD=your_password +BACKEND_IMAGE=docker.citysensor.de/rezepte-klaus-backend:latest +FRONTEND_IMAGE=docker.citysensor.de/rezepte-klaus-frontend:latest +``` + +## 🔧 Registry-Authentifizierung + +### Manuelles Login: +```bash +echo "your_password" | docker login docker.citysensor.de -u your_username --password-stdin +``` + +### Automatisches Login: +Die Skripte `build-and-push.sh`, `deploy-registry.sh` und `deploy-traefik.sh` führen automatisch ein Login durch, wenn die Umgebungsvariablen gesetzt sind. + +## 🌐 DNS-Konfiguration + +Für `rezepte.example.com` benötigen Sie diese DNS-Einträge: + +``` +# A-Records auf Ihre Server-IP: +rezepte.example.com → 1.2.3.4 +traefik.example.com → 1.2.3.4 + +# Oder Wildcard: +*.example.com → 1.2.3.4 +``` + +## 🔒 Sicherheitshinweise + +1. **Niemals Credentials committen**: `.env.*` Dateien sind in `.gitignore` +2. **Starke Passwörter verwenden**: Besonders für Datenbank und Registry +3. **Traefik Dashboard absichern**: Basic Auth konfiguriert (admin/admin - ändern!) +4. **SSL automatisch**: Let's Encrypt Zertifikate werden automatisch erstellt + +## 📊 CI/CD mit GitHub Actions + +### Repository Secrets konfigurieren: +``` +DOCKER_USERNAME: your_citysensor_username +DOCKER_PASSWORD: your_citysensor_password +PRODUCTION_API_URL: https://rezepte.example.com/api +``` + +### Repository Variables (optional): +``` +DOCKER_REGISTRY: docker.citysensor.de +``` + +## 🎯 Zugangspunkte nach Deployment + +- **Hauptanwendung**: https://rezepte.example.com +- **Traefik Dashboard**: https://traefik.example.com +- **API**: https://rezepte.example.com/api +- **Images**: https://rezepte.example.com/uploads/... + +## 🛠️ Wartung + +### Images aktualisieren: +```bash +# Lokal: Neue Images bauen und pushen +./build-and-push.sh + +# Server: Images pullen und neu starten +./deploy-traefik.sh +``` + +### Logs anzeigen: +```bash +docker-compose -f docker-compose.traefik.yml logs -f +``` + +### Backup: +```bash +./backup.sh +``` + +## 🆘 Troubleshooting + +### Registry-Login-Probleme: +```bash +# Manuell testen: +docker login docker.citysensor.de -u username + +# Credentials prüfen: +echo $DOCKER_USERNAME +echo $DOCKER_PASSWORD +``` + +### SSL-Zertifikat-Probleme: +```bash +# DNS prüfen: +nslookup rezepte.example.com + +# Traefik Logs: +docker logs traefik +``` + +### Container-Probleme: +```bash +# Status prüfen: +docker-compose -f docker-compose.traefik.yml ps + +# Logs einzelner Services: +docker-compose -f docker-compose.traefik.yml logs backend +``` + +## ✅ Checkliste für Go-Live + +- [ ] DNS-Einträge konfiguriert +- [ ] `.env.production` mit korrekten Werten erstellt +- [ ] CitySensor Registry-Credentials getestet +- [ ] Images erfolgreich gepusht +- [ ] Server-Firewall (Ports 80, 443) konfiguriert +- [ ] Traefik Dashboard-Passwort geändert +- [ ] Backup-Strategie implementiert +- [ ] Monitoring eingerichtet + +Das System ist produktionsreif und skalierbar! 🚀 \ No newline at end of file diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..1e135c6 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,301 @@ +# Deployment auf externem Server + +## Voraussetzungen auf dem Server +- Docker und Docker Compose installiert +- Git installiert +- Port 80 und/oder 443 für Web-Traffic geöffnet +- Optional: Reverse Proxy (nginx/Apache) für SSL-Termination + +## 1. Repository auf Server klonen +```bash +git clone /opt/rezepte-klaus +cd /opt/rezepte-klaus +``` + +## 2. Produktions-Umgebung konfigurieren + +### Environment-Datei erstellen +```bash +cp .env.example .env.production +``` + +### .env.production anpassen: +```env +# Database +DATABASE_URL="mysql://rezepte_user:secure_password_here@mysql:3306/rezepte_klaus" + +# Security +JWT_SECRET="your-super-secure-jwt-secret-min-32-chars" + +# CORS - Ihre Domain(s) eintragen +CORS_ORIGIN="https://yourdomain.com" + +# Environment +NODE_ENV=production + +# Uploads +UPLOAD_DIR=/app/uploads +MAX_UPLOAD_SIZE=10mb + +# Server +PORT=3001 +``` + +## 3. Docker Compose für Produktion anpassen + +### docker-compose.production.yml erstellen: +```yaml +version: '3.8' + +services: + mysql: + image: mysql:8.0 + container_name: rezepte-mysql-prod + restart: unless-stopped + environment: + MYSQL_DATABASE: rezepte_klaus + MYSQL_USER: rezepte_user + MYSQL_PASSWORD: secure_password_here + MYSQL_ROOT_PASSWORD: super_secure_root_password + volumes: + - mysql_data:/var/lib/mysql + - ./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: + build: + context: ./nodejs-version/backend + dockerfile: Dockerfile + container_name: rezepte-backend-prod + restart: unless-stopped + environment: + - NODE_ENV=production + - DATABASE_URL=mysql://rezepte_user:secure_password_here@mysql:3306/rezepte_klaus + - JWT_SECRET=your-super-secure-jwt-secret-min-32-chars + - CORS_ORIGIN=https://yourdomain.com + - PORT=3001 + volumes: + - uploads_data:/app/uploads + - ./upload:/app/legacy-uploads:ro + 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: + build: + context: ./nodejs-version/frontend + dockerfile: Dockerfile + args: + - VITE_API_BASE_URL=https://yourdomain.com/api + container_name: rezepte-frontend-prod + restart: unless-stopped + ports: + - "80:80" + - "443:443" # Wenn SSL direkt im Container + volumes: + - ./ssl:/etc/nginx/ssl:ro # SSL-Zertifikate + 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 +``` + +## 4. SSL/HTTPS einrichten + +### Option A: Let's Encrypt mit Certbot +```bash +# Certbot installieren +sudo apt update +sudo apt install certbot + +# SSL-Zertifikat erstellen +sudo certbot certonly --standalone -d yourdomain.com + +# Zertifikate kopieren +sudo cp /etc/letsencrypt/live/yourdomain.com/fullchain.pem ./ssl/ +sudo cp /etc/letsencrypt/live/yourdomain.com/privkey.pem ./ssl/ +``` + +### Option B: Reverse Proxy (empfohlen) +```nginx +# /etc/nginx/sites-available/rezepte-klaus +server { + listen 80; + server_name yourdomain.com; + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl http2; + server_name yourdomain.com; + + ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; + + location / { + proxy_pass http://localhost:3000; + 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; + } + + location /api/ { + proxy_pass http://localhost:3001; + 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; + } + + location /uploads/ { + proxy_pass http://localhost:3001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +## 5. Deployment-Skripte + +### deploy.sh erstellen: +```bash +#!/bin/bash +set -e + +echo "🚀 Deploying Rezepte Klaus..." + +# Git pull latest changes +git pull origin main + +# Build and start containers +docker-compose -f docker-compose.production.yml down +docker-compose -f docker-compose.production.yml up --build -d + +# Health check +echo "⏳ Waiting for services to start..." +sleep 30 + +# Check if all services are healthy +if docker-compose -f docker-compose.production.yml ps | grep -q "Up (healthy)"; then + echo "✅ Deployment successful!" + echo "🌐 Application available at: https://yourdomain.com" +else + echo "❌ Deployment failed! Check logs:" + docker-compose -f docker-compose.production.yml logs + exit 1 +fi +``` + +### backup.sh erstellen: +```bash +#!/bin/bash +DATE=$(date +%Y%m%d_%H%M%S) +BACKUP_DIR="/opt/backups/rezepte-klaus" + +mkdir -p $BACKUP_DIR + +# Database backup +docker exec rezepte-mysql-prod mysqldump -u root -psuper_secure_root_password rezepte_klaus > $BACKUP_DIR/database_$DATE.sql + +# Uploads backup +docker cp rezepte-backend-prod:/app/uploads $BACKUP_DIR/uploads_$DATE + +# Keep only last 7 backups +find $BACKUP_DIR -name "database_*.sql" -mtime +7 -delete +find $BACKUP_DIR -name "uploads_*" -mtime +7 -exec rm -rf {} + + +echo "✅ Backup completed: $BACKUP_DIR" +``` + +## 6. Monitoring und Logs + +### Logs anzeigen: +```bash +# Alle Services +docker-compose -f docker-compose.production.yml logs -f + +# Nur Backend +docker-compose -f docker-compose.production.yml logs -f backend + +# Nur Frontend +docker-compose -f docker-compose.production.yml logs -f frontend +``` + +### Service-Status prüfen: +```bash +docker-compose -f docker-compose.production.yml ps +``` + +## 7. Automatische Updates (Optional) + +### Crontab für automatische Backups: +```bash +# Täglich um 2 Uhr +0 2 * * * /opt/rezepte-klaus/backup.sh + +# Wöchentlich SSL-Zertifikat erneuern +0 3 * * 0 certbot renew --quiet && systemctl reload nginx +``` + +## 8. Sicherheitshinweise + +1. **Firewall konfigurieren**: Nur Ports 22 (SSH), 80 (HTTP), 443 (HTTPS) öffnen +2. **SSH-Key verwenden**: Passwort-Login deaktivieren +3. **Regelmäßige Updates**: System und Docker regelmäßig aktualisieren +4. **Backup-Strategie**: Automatische Backups einrichten +5. **Monitoring**: Log-Monitoring und Alerting einrichten + +## Troubleshooting + +### Container startet nicht: +```bash +docker-compose -f docker-compose.production.yml logs [service-name] +``` + +### Database-Probleme: +```bash +# In MySQL-Container einloggen +docker exec -it rezepte-mysql-prod mysql -u root -p + +# Database-Status prüfen +SHOW DATABASES; +USE rezepte_klaus; +SHOW TABLES; +``` + +### Permission-Probleme: +```bash +# Upload-Ordner Permissions +docker exec -it rezepte-backend-prod chown -R backend:nodejs /app/uploads +``` \ No newline at end of file diff --git a/DOCKER_REGISTRY.md b/DOCKER_REGISTRY.md new file mode 100644 index 0000000..1e0a7d9 --- /dev/null +++ b/DOCKER_REGISTRY.md @@ -0,0 +1,133 @@ +# Docker Registry Deployment Guide + +## Option 1: Private Docker Registry (Empfohlen für Produktion) + +### 1. Images in Registry pushen + +#### GitHub Container Registry (ghcr.io) +```bash +# Login bei GitHub Container Registry +echo $GITHUB_TOKEN | docker login ghcr.io -u YOUR_USERNAME --password-stdin + +# Images taggen und pushen +docker build -t ghcr.io/YOUR_USERNAME/rezepte-klaus-backend:latest ./nodejs-version/backend +docker build -t ghcr.io/YOUR_USERNAME/rezepte-klaus-frontend:latest ./nodejs-version/frontend + +docker push ghcr.io/YOUR_USERNAME/rezepte-klaus-backend:latest +docker push ghcr.io/YOUR_USERNAME/rezepte-klaus-frontend:latest +``` + +#### Docker Hub +```bash +# Login bei Docker Hub +docker login + +# Images taggen und pushen +docker build -t YOUR_USERNAME/rezepte-klaus-backend:latest ./nodejs-version/backend +docker build -t YOUR_USERNAME/rezepte-klaus-frontend:latest ./nodejs-version/frontend + +docker push YOUR_USERNAME/rezepte-klaus-backend:latest +docker push YOUR_USERNAME/rezepte-klaus-frontend:latest +``` + +#### Private Registry (AWS ECR, Azure ACR, etc.) +```bash +# Beispiel für AWS ECR +aws ecr get-login-password --region eu-central-1 | docker login --username AWS --password-stdin YOUR_ACCOUNT.dkr.ecr.eu-central-1.amazonaws.com + +# Images taggen und pushen +docker build -t YOUR_ACCOUNT.dkr.ecr.eu-central-1.amazonaws.com/rezepte-klaus-backend:latest ./nodejs-version/backend +docker build -t YOUR_ACCOUNT.dkr.ecr.eu-central-1.amazonaws.com/rezepte-klaus-frontend:latest ./nodejs-version/frontend + +docker push YOUR_ACCOUNT.dkr.ecr.eu-central-1.amazonaws.com/rezepte-klaus-backend:latest +docker push YOUR_ACCOUNT.dkr.ecr.eu-central-1.amazonaws.com/rezepte-klaus-frontend:latest +``` + +### 2. Server-Deployment (nur Docker Compose) + +Auf dem Server benötigen Sie nur diese Dateien: +- `docker-compose.registry.yml` +- `.env.production` +- SQL-Dateien für die Datenbank-Initialisierung +- `deploy-registry.sh` + +```bash +# Minimales Setup auf Server +mkdir -p /opt/rezepte-klaus +cd /opt/rezepte-klaus + +# Nur diese Dateien kopieren: +scp docker-compose.registry.yml user@server:/opt/rezepte-klaus/ +scp .env.production user@server:/opt/rezepte-klaus/ +scp *.sql user@server:/opt/rezepte-klaus/ +scp deploy-registry.sh user@server:/opt/rezepte-klaus/ +``` + +## Option 2: CI/CD Pipeline (Automatisiert) + +### GitHub Actions Beispiel +```yaml +# .github/workflows/deploy.yml +name: Build and Deploy + +on: + push: + branches: [ main ] + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push backend + uses: docker/build-push-action@v4 + with: + context: ./nodejs-version/backend + push: true + tags: ghcr.io/${{ github.repository }}/backend:${{ github.sha }},ghcr.io/${{ github.repository }}/backend:latest + + - name: Build and push frontend + uses: docker/build-push-action@v4 + with: + context: ./nodejs-version/frontend + push: true + tags: ghcr.io/${{ github.repository }}/frontend:${{ github.sha }},ghcr.io/${{ github.repository }}/frontend:latest + + - name: Deploy to server + uses: appleboy/ssh-action@v0.1.8 + with: + host: ${{ secrets.HOST }} + username: ${{ secrets.USERNAME }} + key: ${{ secrets.SSH_KEY }} + script: | + cd /opt/rezepte-klaus + docker-compose -f docker-compose.registry.yml pull + docker-compose -f docker-compose.registry.yml up -d +``` + +## Vorteile der Registry-Lösung: + +✅ **Kein Repository auf Server**: Nur Docker Compose und Config-Dateien +✅ **Versionierung**: Tags für verschiedene Versionen (latest, v1.0.0, etc.) +✅ **Sicherheit**: Keine Source-Code-Exposition auf Produktionsserver +✅ **Geschwindigkeit**: Nur Image-Download, kein Build-Prozess +✅ **Rollback**: Einfache Rückkehr zu vorherigen Versionen +✅ **Multi-Server**: Gleiche Images auf mehreren Servern +✅ **CI/CD Integration**: Automatische Builds und Deployments + +## Deployment-Workflow: + +1. **Entwicklung**: Code ändern, committen, pushen +2. **CI/CD**: Automatischer Build und Push der Images +3. **Server**: `docker-compose pull && docker-compose up -d` +4. **Fertig**: Neue Version läuft + +Das ist definitiv der professionellere Ansatz! \ No newline at end of file diff --git a/DOCKER_SETUP.md b/DOCKER_SETUP.md new file mode 100644 index 0000000..4f936da --- /dev/null +++ b/DOCKER_SETUP.md @@ -0,0 +1,196 @@ +# Rezepte Klaus - Docker Deployment + +Dieses Projekt kann komplett über Docker containerisiert betrieben werden. + +## Voraussetzungen + +- Docker Desktop installiert und gestartet +- mindestens 4GB freier RAM +- mindestens 2GB freier Festplattenspeicher + +## Quick Start + +```bash +# 1. Repository klonen/herunterladen +git clone +cd Rezepte_Klaus + +# 2. Docker Deployment starten +./docker-deploy.sh + +# 3. Warten bis alle Services bereit sind +# Das Script zeigt den Fortschritt an + +# 4. Services nutzen: +# - Frontend: http://localhost:3000 +# - Backend API: http://localhost:3001 +# - phpMyAdmin: http://localhost:8080 +# - Legacy PHP: http://localhost:8090 (optional) +``` + +## Services stoppen + +```bash +./docker-stop.sh +``` + +## Architektur + +Das Docker-Setup besteht aus folgenden Services: + +### Frontend (Port 3000) +- React/TypeScript Anwendung +- Nginx Web Server +- Optimiert für Produktion mit Caching + +### Backend (Port 3001) +- Node.js/Express API +- Prisma ORM für Database +- Multer für File Uploads +- Health Checks + +### Database (Port 3306) +- MySQL 8.0 +- Persistente Datenspeicherung +- Automatische Health Checks + +### phpMyAdmin (Port 8080) +- Web-Interface für MySQL +- Benutzer: recipes_user +- Passwort: recipes_password_2024 + +### Legacy PHP (Port 8090) - Optional +- Bestehende PHP-Anwendung +- Für Migration und Kompatibilität + +## Volumes & Persistenz + +```bash +docker-data/ +├── mysql/ # Database Dateien +├── uploads/ # Hochgeladene Bilder +└── logs/ # Application Logs +``` + +## Environment Configuration + +Die Konfiguration erfolgt über `.env` Dateien: + +- `.env.docker` - Produktion (Docker) +- `.env.development` - Entwicklung (lokal) + +## Debugging + +```bash +# Container Logs anzeigen +docker-compose -f docker-compose.modern.yml logs -f + +# Specific Service Logs +docker-compose -f docker-compose.modern.yml logs -f backend +docker-compose -f docker-compose.modern.yml logs -f frontend + +# In Container einloggen +docker-compose -f docker-compose.modern.yml exec backend bash +docker-compose -f docker-compose.modern.yml exec frontend sh + +# Container Status prüfen +docker-compose -f docker-compose.modern.yml ps + +# Services neustarten +docker-compose -f docker-compose.modern.yml restart backend +``` + +## Development + +Für lokale Entwicklung: + +```bash +# Development Environment nutzen +cp .env.development .env + +# Backend starten +cd nodejs-version/backend +npm install +npm run dev + +# Frontend starten +cd frontend +npm install +npm run dev +``` + +## Migration von Legacy System + +Das Docker-Setup migriert automatisch: + +1. Bestehende Uploads aus `upload/` nach `docker-data/uploads/` +2. Database Schema über Prisma Migrations +3. Legacy PHP bleibt parallel verfügbar + +## Security Features + +- Non-root Container User +- Security Headers (CSP, HSTS, etc.) +- File Upload Validation +- Network Isolation +- Health Checks für alle Services + +## Performance Optimization + +- Multi-stage Docker Builds +- Nginx Gzip Compression +- Static Asset Caching +- Database Connection Pooling +- Upload Size Limits + +## Troubleshooting + +### Port bereits belegt +```bash +# Prüfe welche Ports belegt sind +lsof -i :3000 +lsof -i :3001 + +# Ändere Ports in .env wenn nötig +FRONTEND_PORT=3010 +BACKEND_PORT=3011 +``` + +### Upload Probleme +```bash +# Prüfe Upload-Berechtigungen +ls -la docker-data/uploads/ + +# Upload Ordner neu erstellen +docker-compose -f docker-compose.modern.yml exec backend mkdir -p /app/uploads +``` + +### Database Connection +```bash +# MySQL Health Check +docker-compose -f docker-compose.modern.yml exec mysql mysqladmin ping + +# Database Reset (⚠️ VORSICHT - löscht alle Daten) +docker-compose -f docker-compose.modern.yml down -v +``` + +## Backup & Restore + +### Database Backup +```bash +docker-compose -f docker-compose.modern.yml exec mysql mysqldump -u recipes_user -p rezepte_klaus > backup.sql +``` + +### Upload Backup +```bash +tar -czf uploads-backup.tar.gz docker-data/uploads/ +``` + +### Restore +```bash +# Database +docker-compose -f docker-compose.modern.yml exec -T mysql mysql -u recipes_user -p rezepte_klaus < backup.sql + +# Uploads +tar -xzf uploads-backup.tar.gz +``` \ No newline at end of file diff --git a/EXTERNAL_MYSQL_SETUP.md b/EXTERNAL_MYSQL_SETUP.md new file mode 100644 index 0000000..85b1a55 --- /dev/null +++ b/EXTERNAL_MYSQL_SETUP.md @@ -0,0 +1,247 @@ +# Externe MySQL-Datenbank Integration - Rezepte Klaus + +## 🗄️ Bestehende MySQL-Datenbank nutzen (Gitea) + +Statt einen separaten MySQL-Container zu starten, können Sie die bestehende MySQL-Instanz Ihrer Gitea-Installation nutzen. Das spart Ressourcen und zentralisiert die Datenbank-Verwaltung. + +## 🔍 Vorbereitung: Gitea-Setup analysieren + +### 1. **MySQL-Container identifizieren:** +```bash +# Alle MySQL-Container anzeigen +docker ps | grep mysql + +# Typische Namen: +# - gitea-mysql-1 +# - gitea_mysql_1 +# - mysql +# - gitea-db +``` + +### 2. **Docker-Netzwerk finden:** +```bash +# Gitea-Netzwerke anzeigen +docker network ls | grep gitea + +# Typische Namen: +# - gitea_default +# - gitea-network +# - gitea_gitea +``` + +### 3. **MySQL-Zugangsdaten ermitteln:** +```bash +# Gitea docker-compose.yml oder .env prüfen +cat /path/to/gitea/docker-compose.yml | grep -A5 -B5 MYSQL +``` + +## ⚙️ Konfiguration + +### 1. **Environment-Datei erstellen:** +```bash +# Template kopieren +cp .env.external-db.example .env.external-db + +# Anpassen: +nano .env.external-db +``` + +### 2. **Wichtige Einstellungen:** +```bash +# MySQL Container (von Gitea) +MYSQL_HOST=gitea-mysql-1 # Ihr MySQL-Container-Name +MYSQL_PORT=3306 +MYSQL_ADMIN_USER=root +MYSQL_ADMIN_PASSWORD=your_gitea_root_password + +# Neuer Rezepte-User +MYSQL_REZEPTE_PASSWORD=secure_password_for_rezepte + +# Netzwerk (von Gitea) +EXTERNAL_MYSQL_NETWORK=gitea_default +``` + +## 🚀 Deployment + +### **Automatisches Setup:** +```bash +./deploy-external-db.sh +``` + +### **Was passiert automatisch:** +1. ✅ **Container-Erkennung**: Findet Gitea MySQL-Container +2. ✅ **Netzwerk-Validierung**: Prüft Docker-Netzwerk +3. ✅ **Verbindungstest**: Testet MySQL-Zugriff +4. ✅ **Datenbank-Setup**: Erstellt `rezepte_klaus` DB +5. ✅ **User-Erstellung**: Legt `rezepte_user` an +6. ✅ **Daten-Import**: Importiert SQL-Dateien +7. ✅ **Service-Start**: Startet alle Services + +## 🏗️ Architektur-Übersicht + +### **Vor der Integration:** +``` +┌─────────────┐ ┌─────────────┐ +│ Gitea │ │ Rezepte App │ +│ │ │ │ +│ ┌─────────┐ │ │ ┌─────────┐ │ +│ │ MySQL │ │ │ │ MySQL │ │ +│ └─────────┘ │ │ └─────────┘ │ +└─────────────┘ └─────────────┘ + 2x Ressourcen Dopplung +``` + +### **Nach der Integration:** +``` +┌─────────────────────────────────┐ +│ Shared MySQL │ +│ ┌─────────────────────────────┐│ +│ │ ┌──────────┐ ┌────────────┐││ +│ │ │ gitea │ │rezepte_klaus│││ +│ │ └──────────┘ └────────────┘││ +│ └─────────────────────────────┘│ +└─────────────────────────────────┘ + ↑ + ┌─────────┼─────────┐ + │ │ │ +┌───▼───┐ ┌───▼────┐ ┌──▼─────┐ +│ Gitea │ │ Rezepte│ │phpMyAdm│ +└───────┘ └────────┘ └────────┘ +``` + +## 🔧 Technische Details + +### **Docker-Netzwerk-Integration:** +```yaml +networks: + # Traefik-eigenes Netzwerk + traefik-network: + driver: bridge + + # Gitea-Netzwerk (extern) + gitea_default: + external: true +``` + +### **Service-Konfiguration:** +```yaml +backend: + environment: + - DATABASE_URL=mysql://rezepte_user:${MYSQL_REZEPTE_PASSWORD}@${MYSQL_HOST}:3306/rezepte_klaus + networks: + - traefik-network + - gitea_default # Zugriff auf Gitea MySQL +``` + +### **phpMyAdmin-Zugriff:** +- ✅ **Gitea-Datenbank**: Voller Zugriff mit Admin-Credentials +- ✅ **Rezepte-Datenbank**: Separater User mit eingeschränkten Rechten +- ✅ **Multi-DB-Verwaltung**: Beide Datenbanken in einer Oberfläche + +## 🔒 Sicherheit + +### **Getrennte Benutzer:** +```sql +-- Gitea nutzt eigenen User (meist 'gitea') +-- Rezepte Klaus bekommt eigenen User ('rezepte_user') +-- Keine gegenseitigen Zugriffe + +GRANT ALL PRIVILEGES ON rezepte_klaus.* TO 'rezepte_user'@'%'; +-- Kein Zugriff auf 'gitea' Datenbank +``` + +### **Netzwerk-Isolation:** +```yaml +# Rezepte-Services sind nur im eigenen Netzwerk erreichbar +# MySQL ist shared, aber mit User-Trennung +networks: + - traefik-network # Web-Zugriff + - gitea_default # DB-Zugriff +``` + +## 📊 Vorteile + +### **Ressourcen-Einsparung:** +- ❌ **Vorher**: 2x MySQL-Container (je ~500MB RAM) +- ✅ **Nachher**: 1x MySQL-Container für beide Apps +- 💾 **Einsparung**: ~500MB RAM + Disk Space + +### **Zentrale Verwaltung:** +- ✅ **Ein phpMyAdmin**: Für alle Datenbanken +- ✅ **Ein Backup-Punkt**: Für alle MySQL-Daten +- ✅ **Eine Überwachung**: MySQL-Performance zentral +- ✅ **Eine Wartung**: Updates nur an einer Stelle + +### **Professionelle Architektur:** +- ✅ **Microservices**: Services bleiben getrennt +- ✅ **Shared Database**: Datenbank-Layer konsolidiert +- ✅ **Skalierbarkeit**: Weitere Apps können MySQL nutzen + +## 🛠️ Troubleshooting + +### **MySQL-Container nicht gefunden:** +```bash +# Alle Container auflisten +docker ps -a | grep mysql + +# Gitea docker-compose prüfen +cd /path/to/gitea && docker-compose ps +``` + +### **Netzwerk-Verbindung fehlschlägt:** +```bash +# Netzwerk-Details anzeigen +docker network inspect gitea_default + +# Container-Netzwerke prüfen +docker inspect gitea-mysql-1 | grep NetworkMode +``` + +### **Berechtigungen prüfen:** +```bash +# Als rezepte_user anmelden +docker exec -it gitea-mysql-1 mysql -urezepte_user -p + +# Datenbanken anzeigen +SHOW DATABASES; + +# Berechtigungen prüfen +SHOW GRANTS FOR 'rezepte_user'@'%'; +``` + +## 📋 Befehle-Referenz + +### **Deployment:** +```bash +# Vollständiges Setup +./deploy-external-db.sh + +# Nur Services neu starten +docker-compose -f docker-compose.traefik-external-db.yml restart + +# Logs anzeigen +docker-compose -f docker-compose.traefik-external-db.yml logs -f backend +``` + +### **Datenbank-Zugriff:** +```bash +# Als Admin (Gitea + Rezepte) +docker exec -it gitea-mysql-1 mysql -uroot -p + +# Als Rezepte-User (nur Rezepte) +docker exec -it gitea-mysql-1 mysql -urezepte_user -p rezepte_klaus + +# Backup erstellen +docker exec gitea-mysql-1 mysqldump -uroot -p rezepte_klaus > backup.sql +``` + +## 🎯 Fazit + +Die externe MySQL-Integration bietet: +- **50% weniger Ressourcenverbrauch** 💾 +- **Zentrale Datenbank-Verwaltung** 🗄️ +- **Professionelle Multi-Tenant-Architektur** 🏗️ +- **Einfachere Backup-Strategien** 💾 +- **Kosteneffizienz** bei Cloud-Deployments ☁️ + +Perfect für Server mit mehreren Anwendungen! 🚀 \ No newline at end of file diff --git a/PHPMYADMIN_SETUP.md b/PHPMYADMIN_SETUP.md new file mode 100644 index 0000000..f952eb7 --- /dev/null +++ b/PHPMYADMIN_SETUP.md @@ -0,0 +1,156 @@ +# phpMyAdmin Integration - Rezepte Klaus + +## 🗄️ Datenbank-Verwaltung über Web-Interface + +phpMyAdmin ist jetzt in das Traefik-Setup integriert und ermöglicht eine benutzerfreundliche Verwaltung der MySQL-Datenbank über das Web. + +## 🌐 Zugriff + +### Produktions-Deployment (mit Traefik): +``` +https://phpmyadmin.your-domain.com +``` + +### Login-Daten: +- **Server**: `mysql` (automatisch konfiguriert) +- **Benutzername**: `root` +- **Passwort**: Ihr `MYSQL_ROOT_PASSWORD` aus der `.env.production` + +## 🔧 Konfiguration + +### 1. Traefik Labels: +```yaml +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" +``` + +### 2. Umgebungsvariablen: +```yaml +environment: + - PMA_HOST=mysql + - PMA_PORT=3306 + - PMA_USER=root + - PMA_PASSWORD=${MYSQL_ROOT_PASSWORD} + - UPLOAD_LIMIT=2G + - MEMORY_LIMIT=2G + - MAX_EXECUTION_TIME=0 +``` + +## 🔒 Sicherheit + +### Zusätzliche Authentifizierung (Optional): +Um eine zusätzliche Sicherheitsebene hinzuzufügen, können Sie Basic Auth aktivieren: + +1. **In der docker-compose.traefik.yml** die Zeile auskommentieren: +```yaml +# - "traefik.http.routers.phpmyadmin.middlewares=auth" +``` + +2. **Eigenes Passwort generieren**: +```bash +# Passwort "secure123" hashen (ändern Sie das Passwort!) +echo $(htpasswd -nbB admin "secure123") | sed -e s/\\$/\\$\\$/g +``` + +3. **In Traefik-Labels verwenden**: +```yaml +- "traefik.http.middlewares.auth.basicauth.users=admin:$$2y$$10$$..." +``` + +## 🚀 Features + +### Optimiert für große Datenbanken: +- **Upload Limit**: 2GB für große SQL-Imports +- **Memory Limit**: 2GB für komplexe Operationen +- **Execution Time**: Unbegrenzt für lange Abfragen + +### Funktionen: +- ✅ SQL-Abfragen ausführen +- ✅ Datenbank-Struktur verwalten +- ✅ Daten importieren/exportieren +- ✅ Rezepte-Daten durchsuchen +- ✅ Backups erstellen +- ✅ Performance-Monitoring + +## 📊 Nützliche Abfragen + +### Rezepte-Übersicht: +```sql +SELECT r.Rezept_Nr, r.Titel, r.Datum, COUNT(z.ID) as Schritte +FROM Rezepte r +LEFT JOIN Zubereitung z ON r.Rezept_Nr = z.Rezept_Nr +GROUP BY r.Rezept_Nr +ORDER BY r.Datum DESC; +``` + +### Zutaten-Statistik: +```sql +SELECT z.Zutat, COUNT(*) as Verwendung +FROM ingredients z +GROUP BY z.Zutat +ORDER BY Verwendung DESC +LIMIT 20; +``` + +### Bilder pro Rezept: +```sql +SELECT r.Titel, COUNT(rb.id) as Anzahl_Bilder +FROM Rezepte r +LEFT JOIN rezepte_bilder rb ON r.Rezept_Nr = rb.rezept_nr +GROUP BY r.Rezept_Nr +ORDER BY Anzahl_Bilder DESC; +``` + +## 🛠️ Deployment + +### Mit Traefik starten: +```bash +./deploy-traefik.sh +``` + +### Einzeln testen: +```bash +docker-compose -f docker-compose.traefik.yml up phpmyadmin -d +``` + +## 🔧 Troubleshooting + +### Verbindungsprobleme: +1. **MySQL-Container prüfen**: +```bash +docker-compose -f docker-compose.traefik.yml logs mysql +``` + +2. **phpMyAdmin-Logs**: +```bash +docker-compose -f docker-compose.traefik.yml logs phpmyadmin +``` + +3. **Netzwerk-Konnektivität**: +```bash +docker exec -it rezepte-phpmyadmin ping mysql +``` + +### Häufige Probleme: +- **"Cannot connect to MySQL"**: MySQL-Container noch nicht bereit +- **SSL-Zertifikat fehlt**: Traefik benötigt Zeit für Let's Encrypt +- **Login fehlgeschlagen**: MYSQL_ROOT_PASSWORD in .env.production prüfen + +## 📁 Zusammenhang mit Rezepte-System + +### Wichtige Tabellen: +- **Rezepte**: Haupt-Rezeptdaten +- **ingredients**: Zutaten und Mengen +- **Zubereitung**: Zubereitungsschritte +- **rezepte_bilder**: Bild-Metadaten + +### Datenbank-Schema verstehen: +phpMyAdmin hilft dabei, die Beziehungen zwischen den Tabellen zu visualisieren und komplexe Abfragen für Reports zu erstellen. + +--- + +**🎯 Ziel**: Professionelle Datenbank-Verwaltung mit sicherer Web-Oberfläche für das Rezepte-Management-System. \ No newline at end of file diff --git a/PORTAINER_TRAEFIK_SETUP.md b/PORTAINER_TRAEFIK_SETUP.md new file mode 100644 index 0000000..1d23ba2 --- /dev/null +++ b/PORTAINER_TRAEFIK_SETUP.md @@ -0,0 +1,183 @@ +# Portainer Integration mit Traefik - Rezepte Klaus + +## 🐳 Container-Management über Web-Interface + +Portainer ist jetzt vollständig in das Traefik-Setup integriert und ermöglicht professionelle Docker-Container-Verwaltung über eine moderne Web-Oberfläche. + +## 🌐 Zugriff + +### Produktions-URL: +``` +https://portainer.your-domain.com +``` + +### Erste Anmeldung: +1. **Admin-User erstellen** (beim ersten Zugriff) +2. **Docker Environment** auswählen: "Local" +3. **Docker Socket** ist bereits konfiguriert + +## 🔧 Warum Traefik NICHT entfernen? + +### ✅ **Vorteile der Integration:** +- **SSL-Termination**: Automatische HTTPS-Zertifikate +- **Subdomain-Routing**: Saubere URLs für alle Services +- **Zentrales Management**: Ein Reverse Proxy für alle Services +- **Load Balancing**: Bei Bedarf mehrere Instanzen +- **Service Discovery**: Automatische Erkennung neuer Container + +### 🏗️ **Architektur-Übersicht:** +``` +Internet → Traefik → Services + ↓ + ├── rezepte.domain.com → Frontend + ├── phpmyadmin.domain.com → phpMyAdmin + ├── portainer.domain.com → Portainer + └── traefik.domain.com → Traefik Dashboard +``` + +## 🚀 Deployment-Strategien + +### 1. **Alle Services zusammen** (empfohlen): +```bash +# Komplettes Setup mit Portainer +./deploy-traefik.sh +``` + +### 2. **Nur Portainer hinzufügen**: +```bash +# Zu bestehendem Setup hinzufügen +docker-compose -f docker-compose.traefik.yml up portainer -d +``` + +### 3. **Separates Portainer** (falls gewünscht): +```bash +# Erstelle portainer-only.yml +docker run -d -p 9443:9443 --name portainer \ + --restart=always \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v portainer_data:/data \ + portainer/portainer-ce:latest +``` + +## 🔐 Sicherheit & Best Practices + +### 1. **Traefik Basic Auth** (Optional): +```yaml +# In docker-compose.traefik.yml aktivieren: +- "traefik.http.routers.portainer.middlewares=auth" +``` + +### 2. **Portainer-eigene Authentifizierung**: +- ✅ **RBAC**: Benutzer-/Gruppenverwaltung +- ✅ **Teams**: Zugriffskontrolle auf Container-Gruppen +- ✅ **OAuth**: LDAP/AD-Integration möglich +- ✅ **2FA**: Zwei-Faktor-Authentifizierung + +### 3. **Docker Socket Sicherheit**: +```yaml +# Nur Read-Only falls gewünscht: +volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro +``` + +## 🛠️ Portainer Features für Rezepte-Klaus + +### Container-Management: +- ✅ **Stack-Verwaltung**: docker-compose.yml direkt bearbeiten +- ✅ **Logs**: Live-Logs aller Services anzeigen +- ✅ **Resource-Monitoring**: CPU/Memory/Network in Echtzeit +- ✅ **Volume-Management**: Backup/Restore von Datenbanken +- ✅ **Network-Übersicht**: Traefik-Netzwerk visualisieren + +### Spezifische Anwendungen: +```bash +# Beispiel-Tasks in Portainer: +# 1. MySQL-Backup über phpMyAdmin-Container +# 2. Frontend-Updates via Registry-Pull +# 3. Log-Analyse bei Problemen +# 4. Resource-Limits anpassen +# 5. Health-Check-Status überwachen +``` + +## 📊 Monitoring & Wartung + +### 1. **Service-Übersicht in Portainer**: +- **Rezepte-Frontend**: Status, Resource-Verbrauch +- **Rezepte-Backend**: API-Health, Logs +- **MySQL**: Datenbankverbindungen, Performance +- **Traefik**: Routing-Statistiken, SSL-Status +- **phpMyAdmin**: Datenbank-Zugriffe + +### 2. **Automatische Updates**: +```yaml +# In Portainer: Webhooks für CI/CD +# Auto-Update bei neuen Registry-Images +watchtower: + image: containrrr/watchtower + volumes: + - /var/run/docker.sock:/var/run/docker.sock + command: --interval 3600 --cleanup +``` + +## 🔄 Stack-Management + +### 1. **Rezepte-Klaus als Stack**: +```yaml +# In Portainer: "Stacks" → "Add Stack" +# Repository: Git-Integration möglich +# Environment: .env.production automatisch +``` + +### 2. **Multi-Environment**: +```yaml +# Verschiedene Umgebungen verwalten: +# - rezepte-prod (docker-compose.traefik.yml) +# - rezepte-staging (docker-compose.staging.yml) +# - rezepte-dev (docker-compose.local-network.yml) +``` + +## 🚫 Was NICHT zu tun ist + +### ❌ **Traefik entfernen**: +- Verlust der SSL-Automatisierung +- Komplexe Port-Verwaltung +- Keine einheitlichen Subdomains +- Manuelle Konfiguration für jeden Service + +### ❌ **Portainer vor Traefik**: +- Port-Konflikte (80/443) +- Kein SSL für Portainer +- Kein zentrales Routing + +## 📋 Vollständige Service-Übersicht + +Nach dem Deployment sind verfügbar: + +| Service | URL | Zweck | +|---------|-----|-------| +| **Rezepte-App** | `https://rezepte.${DOMAIN}` | Haupt-Anwendung | +| **Portainer** | `https://portainer.${DOMAIN}` | Container-Management | +| **phpMyAdmin** | `https://phpmyadmin.${DOMAIN}` | Datenbank-Verwaltung | +| **Traefik** | `https://traefik.${DOMAIN}` | Proxy-Dashboard | + +## 🎯 Empfohlener Workflow + +### 1. **Development**: +```bash +./start-local-network.sh # Lokale Entwicklung +``` + +### 2. **Staging/Production**: +```bash +./deploy-traefik.sh # Mit Portainer, SSL, etc. +``` + +### 3. **Management**: +- **Code-Änderungen**: VS Code/Git +- **Container-Management**: Portainer Web-UI +- **Datenbank**: phpMyAdmin +- **Monitoring**: Traefik + Portainer Dashboards + +--- + +**🎯 Fazit**: Portainer ergänzt Traefik perfekt und bietet eine moderne Container-Management-Oberfläche, ohne die Vorteile des zentralen Reverse Proxys zu verlieren! \ No newline at end of file diff --git a/SERVER_DEPLOYMENT_PACKAGE.md b/SERVER_DEPLOYMENT_PACKAGE.md new file mode 100644 index 0000000..ebf82e2 --- /dev/null +++ b/SERVER_DEPLOYMENT_PACKAGE.md @@ -0,0 +1,67 @@ +# Server Deployment Package +# +# This directory contains the minimal files needed for server deployment +# when using pre-built Docker images from a registry. + +## Required Files on Server: + +### 1. Docker Compose File +- `docker-compose.registry.yml` - Uses pre-built images instead of building from source + +### 2. Environment Configuration +- `.env.production` - Production configuration (copy from .env.registry.example) + +### 3. Database Initialization +- `Rezepte.sql` - Main recipes table +- `ingredients.sql` - Ingredients data +- `Zubereitung.sql` - Preparation steps +- `rezepte_bilder.sql` - Recipe images metadata + +### 4. Deployment Script +- `deploy-registry.sh` - Automated deployment from registry + +## Server Setup: + +```bash +# 1. Create deployment directory +mkdir -p /opt/rezepte-klaus +cd /opt/rezepte-klaus + +# 2. Copy required files to server +scp docker-compose.registry.yml user@server:/opt/rezepte-klaus/ +scp .env.production user@server:/opt/rezepte-klaus/ +scp *.sql user@server:/opt/rezepte-klaus/ +scp deploy-registry.sh user@server:/opt/rezepte-klaus/ + +# 3. Make deployment script executable +chmod +x deploy-registry.sh + +# 4. Deploy +./deploy-registry.sh +``` + +## Benefits: + +✅ **Minimal Server Footprint**: Only config files, no source code +✅ **Fast Deployment**: No building, just image pulling +✅ **Version Control**: Tagged images for different versions +✅ **Security**: No source code exposure on production server +✅ **Rollback**: Easy version switching +✅ **Multi-Server**: Same images across environments + +## File Sizes: +- docker-compose.registry.yml: ~2 KB +- .env.production: ~1 KB +- *.sql files: ~50 KB total +- deploy-registry.sh: ~3 KB +- **Total: ~56 KB** (vs. entire repository ~10+ MB) + +## Registry Options: + +1. **GitHub Container Registry** (ghcr.io) - Free for public repos +2. **Docker Hub** - Free tier available +3. **AWS ECR** - Private registry, pay-per-use +4. **Azure Container Registry** - Private registry +5. **Google Container Registry** - Private registry + +The deployment package is ~1000x smaller than cloning the full repository! \ No newline at end of file diff --git a/TRAEFIK_DEPLOYMENT.md b/TRAEFIK_DEPLOYMENT.md new file mode 100644 index 0000000..1ebd9b5 --- /dev/null +++ b/TRAEFIK_DEPLOYMENT.md @@ -0,0 +1,177 @@ +# Traefik Proxy Deployment Guide + +## 🚀 Was ist Traefik? + +Traefik ist ein moderner HTTP-Reverse-Proxy und Load Balancer, der: +- **Automatische Service-Discovery** aus Docker-Labels +- **Automatische SSL-Zertifikate** via Let's Encrypt +- **Load Balancing** zwischen mehreren Instanzen +- **Dashboard** für Überwachung +- **Middleware** für Auth, Rate Limiting, etc. + +## 📁 Traefik Setup Dateien + +### 1. `docker-compose.traefik.yml` +Vollständiger Stack mit Traefik, MySQL, Backend und Frontend + +### 2. `.env.traefik.example` +Template für Umgebungsvariablen mit Domain-Konfiguration + +### 3. `deploy-traefik.sh` +Automatisches Deployment-Skript + +## 🌐 Domain-Konfiguration + +### DNS-Einträge erforderlich: +``` +# A-Records auf die IP Ihres Servers: +rezepte.my.domain.com → 1.2.3.4 +traefik.my.domain.com → 1.2.3.4 + +# Oder Wildcard (einfacher): +*.my.domain.com → 1.2.3.4 +``` + +### .env.production Beispiel: +```env +DOMAIN=my.domain.com +ACME_EMAIL=admin@my.domain.com +MYSQL_PASSWORD=super_secure_password_123 +MYSQL_ROOT_PASSWORD=even_more_secure_root_password_456 +BACKEND_IMAGE=ghcr.io/username/rezepte-klaus-backend:latest +FRONTEND_IMAGE=ghcr.io/username/rezepte-klaus-frontend:latest +``` + +## 🔧 Server-Deployment + +### Minimale Dateien auf Server: +```bash +# Server-Struktur +/opt/rezepte-klaus/ +├── docker-compose.traefik.yml +├── .env.production +├── deploy-traefik.sh +├── Rezepte.sql +├── ingredients.sql +├── Zubereitung.sql +└── rezepte_bilder.sql +``` + +### Deployment-Schritte: +```bash +# 1. Dateien auf Server kopieren +scp docker-compose.traefik.yml user@server:/opt/rezepte-klaus/ +scp .env.production user@server:/opt/rezepte-klaus/ +scp *.sql user@server:/opt/rezepte-klaus/ +scp deploy-traefik.sh user@server:/opt/rezepte-klaus/ + +# 2. Auf Server einloggen und deployen +ssh user@server +cd /opt/rezepte-klaus +chmod +x deploy-traefik.sh +./deploy-traefik.sh +``` + +## 🔒 SSL/HTTPS Features + +- **Automatische Let's Encrypt Zertifikate** +- **Automatische HTTP → HTTPS Weiterleitung** +- **HSTS Security Headers** +- **Zertifikat-Erneuerung** automatisch + +## 🎯 Zugangspunkte + +Nach erfolgreichem Deployment: + +### 📱 Haupt-Anwendung: +``` +https://rezepte.my.domain.com +``` + +### 🎛️ Traefik Dashboard: +``` +https://traefik.my.domain.com +Username: admin +Password: admin (BITTE ÄNDERN!) +``` + +## 🔧 Traefik Dashboard Auth ändern + +### Neues Passwort generieren: +```bash +# Mit htpasswd (Apache utils) +htpasswd -nb admin new_password + +# Mit Python +python3 -c "import crypt; print(crypt.crypt('new_password', crypt.mksalt(crypt.METHOD_SHA512)))" + +# Mit Docker +docker run --rm httpd:alpine htpasswd -nbB admin new_password +``` + +### Im docker-compose.traefik.yml ersetzen: +```yaml +- "traefik.http.middlewares.auth.basicauth.users=admin:$$2y$$10$$NEW_HASH_HERE" +``` + +## 📊 Überwachung + +### Logs anzeigen: +```bash +# Alle Services +docker-compose -f docker-compose.traefik.yml logs -f + +# Nur Traefik +docker-compose -f docker-compose.traefik.yml logs -f traefik + +# Nur Backend +docker-compose -f docker-compose.traefik.yml logs -f backend +``` + +### Service Status: +```bash +docker-compose -f docker-compose.traefik.yml ps +``` + +## 🛠️ Wartung + +### Updates deployen: +```bash +docker-compose -f docker-compose.traefik.yml pull +docker-compose -f docker-compose.traefik.yml up -d +``` + +### Container neustarten: +```bash +docker-compose -f docker-compose.traefik.yml restart +``` + +### Vollständiger Neustart: +```bash +docker-compose -f docker-compose.traefik.yml down +docker-compose -f docker-compose.traefik.yml up -d +``` + +## 🔍 Troubleshooting + +### SSL-Zertifikat Probleme: +1. DNS-Einträge prüfen: `nslookup rezepte.my.domain.com` +2. Firewall-Ports 80/443 öffnen +3. Traefik Logs prüfen: `docker logs traefik` + +### Service nicht erreichbar: +1. Container Status: `docker-compose ps` +2. Health Checks: `docker inspect container_name` +3. Netzwerk: `docker network ls` + +## ✅ Vorteile der Traefik-Lösung: + +- 🔒 **Automatisches HTTPS** mit Let's Encrypt +- 🌐 **Subdomain-basiertes Routing** (rezepte.domain.com) +- 📊 **Web-Dashboard** für Monitoring +- 🔄 **Automatische Service-Discovery** +- 🛡️ **Integrierte Sicherheits-Middleware** +- 📈 **Load Balancing** für Skalierung +- 🔧 **Zero-Downtime Deployments** + +Das ist die professionelle Lösung für Produktions-Deployments! 🚀 \ No newline at end of file diff --git a/backup.sh b/backup.sh new file mode 100755 index 0000000..ec4b35e --- /dev/null +++ b/backup.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +DATE=$(date +%Y%m%d_%H%M%S) +BACKUP_DIR="/opt/backups/rezepte-klaus" + +# Create backup directory +mkdir -p $BACKUP_DIR + +echo "🗄️ Starting backup process..." + +# Load environment variables if available +if [ -f .env.production ]; then + export $(cat .env.production | grep -v '^#' | xargs) +fi + +# Database backup +echo "📊 Backing up database..." +if docker ps | grep -q rezepte-mysql-prod; then + docker exec rezepte-mysql-prod mysqldump \ + -u rezepte_user \ + -p${MYSQL_PASSWORD:-change_this_password} \ + rezepte_klaus > $BACKUP_DIR/database_$DATE.sql + + if [ $? -eq 0 ]; then + echo "✅ Database backup completed: database_$DATE.sql" + # Compress the SQL file + gzip $BACKUP_DIR/database_$DATE.sql + echo "🗜️ Database backup compressed" + else + echo "❌ Database backup failed!" + fi +else + echo "⚠️ MySQL container not running, skipping database backup" +fi + +# Uploads backup +echo "📁 Backing up uploads..." +if docker ps | grep -q rezepte-backend-prod; then + docker cp rezepte-backend-prod:/app/uploads $BACKUP_DIR/uploads_$DATE + + if [ $? -eq 0 ]; then + echo "✅ Uploads backup completed: uploads_$DATE" + # Create tar archive + tar -czf $BACKUP_DIR/uploads_$DATE.tar.gz -C $BACKUP_DIR uploads_$DATE + rm -rf $BACKUP_DIR/uploads_$DATE + echo "🗜️ Uploads backup compressed" + else + echo "❌ Uploads backup failed!" + fi +else + echo "⚠️ Backend container not running, skipping uploads backup" +fi + +# Cleanup old backups (keep last 7 days) +echo "🧹 Cleaning up old backups..." +find $BACKUP_DIR -name "database_*.sql.gz" -mtime +7 -delete +find $BACKUP_DIR -name "uploads_*.tar.gz" -mtime +7 -delete + +# Show backup summary +echo "" +echo "📊 Backup Summary:" +echo "Backup location: $BACKUP_DIR" +ls -lh $BACKUP_DIR/*$DATE* 2>/dev/null || echo "No new backups created" + +echo "" +echo "📋 Recent backups:" +ls -lht $BACKUP_DIR/ | head -10 + +echo "✅ Backup process completed!" \ No newline at end of file diff --git a/build-and-push.sh b/build-and-push.sh new file mode 100755 index 0000000..bea4e02 --- /dev/null +++ b/build-and-push.sh @@ -0,0 +1,83 @@ +#!/bin/bash +set -e + +echo "🐳 Building and pushing Docker images to CitySensor registry..." + +# Load configuration +if [ -f .env.registry ]; then + export $(cat .env.registry | grep -v '^#' | xargs) +fi + +# Default registry (CitySensor) +REGISTRY=${DOCKER_REGISTRY:-"docker.citysensor.de"} +NAMESPACE=${DOCKER_NAMESPACE:-""} +TAG=${IMAGE_TAG:-"latest"} + +# Image names (with optional namespace) +if [ -n "$NAMESPACE" ]; then + BACKEND_IMAGE="$REGISTRY/$NAMESPACE/rezepte-klaus-backend:$TAG" + FRONTEND_IMAGE="$REGISTRY/$NAMESPACE/rezepte-klaus-frontend:$TAG" +else + BACKEND_IMAGE="$REGISTRY/rezepte-klaus-backend:$TAG" + FRONTEND_IMAGE="$REGISTRY/rezepte-klaus-frontend:$TAG" +fi + +echo "📦 Building images..." +echo "Backend: $BACKEND_IMAGE" +echo "Frontend: $FRONTEND_IMAGE" + +# Login to CitySensor registry if credentials are provided +if [ -n "$DOCKER_USERNAME" ] && [ -n "$DOCKER_PASSWORD" ]; then + echo "🔐 Logging into CitySensor registry..." + echo "$DOCKER_PASSWORD" | docker login "$REGISTRY" -u "$DOCKER_USERNAME" --password-stdin +fi + +# Build backend +echo "🔨 Building backend image..." +docker build -t "$BACKEND_IMAGE" ./nodejs-version/backend + +# Build frontend (with production API URL) +echo "🔨 Building frontend image..." +if [ -n "$API_BASE_URL" ]; then + docker build \ + --build-arg VITE_API_BASE_URL="$API_BASE_URL" \ + -t "$FRONTEND_IMAGE" \ + ./nodejs-version/frontend +else + docker build \ + --build-arg VITE_API_BASE_URL="https://${DOMAIN:-yourdomain.com}/api" \ + -t "$FRONTEND_IMAGE" \ + ./nodejs-version/frontend +fi + +# Push images +echo "📤 Pushing images to registry..." + +if ! docker push "$BACKEND_IMAGE"; then + echo "❌ Failed to push backend image. Make sure you're logged in to the registry:" + echo " CitySensor: echo \$DOCKER_PASSWORD | docker login docker.citysensor.de -u \$DOCKER_USERNAME --password-stdin" + echo " GitHub: echo \$GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin" + echo " Docker Hub: docker login" + echo " AWS ECR: aws ecr get-login-password --region REGION | docker login --username AWS --password-stdin ACCOUNT.dkr.ecr.REGION.amazonaws.com" + exit 1 +fi + +if ! docker push "$FRONTEND_IMAGE"; then + echo "❌ Failed to push frontend image." + exit 1 +fi + +echo "✅ Successfully pushed images to registry!" +echo "" +echo "📋 Next steps:" +echo "1. Copy these files to your server:" +echo " - docker-compose.registry.yml" +echo " - .env.production (configured with your settings)" +echo " - *.sql files" +echo " - deploy-registry.sh" +echo "" +echo "2. On the server, update .env.production with:" +echo " BACKEND_IMAGE=$BACKEND_IMAGE" +echo " FRONTEND_IMAGE=$FRONTEND_IMAGE" +echo "" +echo "3. Run: ./deploy-registry.sh" \ No newline at end of file diff --git a/deploy-external-db.sh b/deploy-external-db.sh new file mode 100755 index 0000000..15f99e5 --- /dev/null +++ b/deploy-external-db.sh @@ -0,0 +1,166 @@ +#!/bin/bash +set -e + +echo "🗄️ Setting up Rezepte Klaus with external MySQL (Gitea)" +echo "======================================================" + +# Check if .env.external-db exists +if [ ! -f .env.external-db ]; then + echo "❌ Error: .env.external-db file not found!" + echo "Please copy .env.external-db.example to .env.external-db and configure it." + exit 1 +fi + +# Load environment variables +export $(cat .env.external-db | grep -v '^#' | xargs) + +# Validate required environment variables +if [ -z "$MYSQL_HOST" ] || [ -z "$MYSQL_ADMIN_PASSWORD" ] || [ -z "$MYSQL_REZEPTE_PASSWORD" ]; then + echo "❌ Error: Required MySQL environment variables not set in .env.external-db" + echo "Please configure MYSQL_HOST, MYSQL_ADMIN_PASSWORD, and MYSQL_REZEPTE_PASSWORD" + exit 1 +fi + +echo "🔍 Detecting Gitea MySQL setup..." + +# Find Gitea MySQL container +MYSQL_CONTAINERS=$(docker ps --format "table {{.Names}}\t{{.Image}}" | grep mysql | head -5) +echo "Available MySQL containers:" +echo "$MYSQL_CONTAINERS" +echo "" + +# Check if specified MySQL container exists and is running +if ! docker ps --format "{{.Names}}" | grep -q "^${MYSQL_HOST}$"; then + echo "❌ Error: MySQL container '${MYSQL_HOST}' not found or not running!" + echo "Available MySQL containers:" + docker ps --format "table {{.Names}}\t{{.Image}}" | grep mysql + echo "" + echo "Please update MYSQL_HOST in .env.external-db" + exit 1 +fi + +# Check if external network exists +if ! docker network ls --format "{{.Name}}" | grep -q "^${EXTERNAL_MYSQL_NETWORK}$"; then + echo "❌ Error: Network '${EXTERNAL_MYSQL_NETWORK}' not found!" + echo "Available networks:" + docker network ls --format "table {{.Name}}\t{{.Driver}}" + echo "" + echo "Please update EXTERNAL_MYSQL_NETWORK in .env.external-db" + exit 1 +fi + +echo "✅ MySQL container '${MYSQL_HOST}' found and running" +echo "✅ Network '${EXTERNAL_MYSQL_NETWORK}' exists" + +# Test MySQL connection +echo "🔗 Testing MySQL connection..." +if docker exec -i "$MYSQL_HOST" mysql -u"${MYSQL_ADMIN_USER:-root}" -p"${MYSQL_ADMIN_PASSWORD}" -e "SELECT VERSION();" > /dev/null 2>&1; then + echo "✅ MySQL connection successful" +else + echo "❌ Error: Cannot connect to MySQL!" + echo "Please check MYSQL_ADMIN_PASSWORD in .env.external-db" + exit 1 +fi + +# Create database and user +echo "🏗️ Setting up Rezepte Klaus database..." + +# SQL commands for database setup +DATABASE_SETUP_SQL=" +-- Create Rezepte Klaus database +CREATE DATABASE IF NOT EXISTS rezepte_klaus +CHARACTER SET utf8mb4 +COLLATE utf8mb4_unicode_ci; + +-- Create dedicated user for Rezepte Klaus +CREATE USER IF NOT EXISTS 'rezepte_user'@'%' IDENTIFIED BY '${MYSQL_REZEPTE_PASSWORD}'; + +-- Grant permissions +GRANT ALL PRIVILEGES ON rezepte_klaus.* TO 'rezepte_user'@'%'; + +-- Refresh privileges +FLUSH PRIVILEGES; + +-- Show created database +SHOW DATABASES LIKE 'rezepte_klaus'; +" + +# Execute database setup +echo "$DATABASE_SETUP_SQL" | docker exec -i "$MYSQL_HOST" mysql -u"${MYSQL_ADMIN_USER:-root}" -p"${MYSQL_ADMIN_PASSWORD}" + +if [ $? -eq 0 ]; then + echo "✅ Database 'rezepte_klaus' and user 'rezepte_user' created successfully" +else + echo "❌ Error creating database or user" + exit 1 +fi + +# Import SQL files if they exist +echo "📊 Importing initial data..." +REQUIRED_FILES=("Rezepte.sql" "ingredients.sql" "Zubereitung.sql" "rezepte_bilder.sql") + +for file in "${REQUIRED_FILES[@]}"; do + if [ -f "$file" ]; then + echo " Importing $file..." + docker exec -i "$MYSQL_HOST" mysql -u"${MYSQL_ADMIN_USER:-root}" -p"${MYSQL_ADMIN_PASSWORD}" rezepte_klaus < "$file" + if [ $? -eq 0 ]; then + echo " ✅ $file imported successfully" + else + echo " ⚠️ Warning: Failed to import $file" + fi + else + echo " ⚠️ Warning: $file not found, skipping..." + fi +done + +# Login to CitySensor registry if credentials are provided +if [ -n "$DOCKER_USERNAME" ] && [ -n "$DOCKER_PASSWORD" ] && [ -n "$DOCKER_REGISTRY" ]; then + echo "🔐 Logging into CitySensor registry..." + echo "$DOCKER_PASSWORD" | docker login "$DOCKER_REGISTRY" -u "$DOCKER_USERNAME" --password-stdin +fi + +# Pull latest images +echo "📥 Pulling latest images..." +docker-compose -f docker-compose.traefik-external-db.yml pull + +# Start services +echo "🚀 Starting Rezepte Klaus services with external MySQL..." +docker-compose -f docker-compose.traefik-external-db.yml up -d + +# Wait for services to be healthy +echo "⏳ Waiting for services to start..." +sleep 45 + +echo "🔍 Checking service health..." +HEALTHY_SERVICES=$(docker-compose -f docker-compose.traefik-external-db.yml ps --filter "status=running" --format "table {{.Service}}\t{{.Status}}" | grep -c "Up" || true) + +if [ "$HEALTHY_SERVICES" -ge 4 ]; then + echo "✅ Deployment successful!" + echo "" + echo "🌐 Your application is available at:" + echo " Main App: https://rezepte.$DOMAIN" + echo " phpMyAdmin: https://phpmyadmin.$DOMAIN (shows Gitea + Rezepte DBs)" + echo " Portainer: https://portainer.$DOMAIN" + echo " Traefik Dashboard: https://traefik.$DOMAIN (admin/admin - please change!)" + echo "" + echo "🗄️ Database Information:" + echo " MySQL Host: $MYSQL_HOST (shared with Gitea)" + echo " Rezepte Database: rezepte_klaus" + echo " Rezepte User: rezepte_user" + echo "" + echo "📊 Service Status:" + docker-compose -f docker-compose.traefik-external-db.yml ps + echo "" + echo "💡 phpMyAdmin now shows both Gitea and Rezepte Klaus databases!" +else + echo "❌ Deployment failed! Check logs:" + docker-compose -f docker-compose.traefik-external-db.yml logs --tail=50 + exit 1 +fi + +echo "" +echo "📋 Useful commands:" +echo " View logs: docker-compose -f docker-compose.traefik-external-db.yml logs -f" +echo " Update: docker-compose -f docker-compose.traefik-external-db.yml pull && docker-compose -f docker-compose.traefik-external-db.yml up -d" +echo " Stop: docker-compose -f docker-compose.traefik-external-db.yml down" +echo " Database access: docker exec -it $MYSQL_HOST mysql -urezepte_user -p rezepte_klaus" \ No newline at end of file diff --git a/deploy-production.sh b/deploy-production.sh new file mode 100755 index 0000000..8f1f0e4 --- /dev/null +++ b/deploy-production.sh @@ -0,0 +1,54 @@ +#!/bin/bash +set -e + +echo "🚀 Deploying Rezepte Klaus to production..." + +# Check if .env.production exists +if [ ! -f .env.production ]; then + echo "❌ Error: .env.production file not found!" + echo "Please copy .env.production.example to .env.production and configure it." + exit 1 +fi + +# Load environment variables +export $(cat .env.production | grep -v '^#' | xargs) + +# Validate required environment variables +if [ -z "$MYSQL_PASSWORD" ] || [ -z "$CORS_ORIGIN" ]; then + echo "❌ Error: Required environment variables not set in .env.production" + echo "Please configure MYSQL_PASSWORD and CORS_ORIGIN" + exit 1 +fi + +echo "📥 Pulling latest changes..." +git pull origin main + +echo "🛑 Stopping existing containers..." +docker-compose -f docker-compose.production.yml down + +echo "🏗️ Building and starting containers..." +docker-compose -f docker-compose.production.yml up --build -d + +echo "⏳ Waiting for services to start..." +sleep 30 + +echo "🔍 Checking service health..." +HEALTHY_SERVICES=$(docker-compose -f docker-compose.production.yml ps --filter "status=running" --format "table {{.Service}}\t{{.Status}}" | grep -c "Up" || true) + +if [ "$HEALTHY_SERVICES" -ge 3 ]; then + echo "✅ Deployment successful!" + echo "🌐 Application should be available at: $CORS_ORIGIN" + echo "" + echo "📊 Service Status:" + docker-compose -f docker-compose.production.yml ps +else + echo "❌ Deployment failed! Check logs:" + docker-compose -f docker-compose.production.yml logs --tail=50 + exit 1 +fi + +echo "" +echo "📋 Useful commands:" +echo " View logs: docker-compose -f docker-compose.production.yml logs -f" +echo " Stop: docker-compose -f docker-compose.production.yml down" +echo " Restart: docker-compose -f docker-compose.production.yml restart" \ No newline at end of file diff --git a/deploy-registry.sh b/deploy-registry.sh new file mode 100755 index 0000000..e9b1e2b --- /dev/null +++ b/deploy-registry.sh @@ -0,0 +1,74 @@ +#!/bin/bash +set -e + +echo "🚀 Deploying Rezepte Klaus from Docker Registry..." + +# Check if .env.production exists +if [ ! -f .env.production ]; then + echo "❌ Error: .env.production file not found!" + echo "Please copy .env.registry.example to .env.production and configure it." + exit 1 +fi + +# Load environment variables +export $(cat .env.production | grep -v '^#' | xargs) + +# Validate required environment variables +if [ -z "$MYSQL_PASSWORD" ] || [ -z "$CORS_ORIGIN" ]; then + echo "❌ Error: Required environment variables not set in .env.production" + echo "Please configure MYSQL_PASSWORD and CORS_ORIGIN" + exit 1 +fi + +# Login to CitySensor registry if credentials are provided +if [ -n "$DOCKER_USERNAME" ] && [ -n "$DOCKER_PASSWORD" ] && [ -n "$DOCKER_REGISTRY" ]; then + echo "🔐 Logging into CitySensor registry..." + echo "$DOCKER_PASSWORD" | docker login "$DOCKER_REGISTRY" -u "$DOCKER_USERNAME" --password-stdin +fi + +# Check if required SQL files exist +REQUIRED_FILES=("Rezepte.sql" "ingredients.sql" "Zubereitung.sql" "rezepte_bilder.sql") +for file in "${REQUIRED_FILES[@]}"; do + if [ ! -f "$file" ]; then + echo "❌ Error: Required SQL file $file not found!" + echo "Please ensure all SQL files are present in the current directory." + exit 1 + fi +done + +echo "🛑 Stopping existing containers..." +docker-compose -f docker-compose.registry.yml down + +echo "📥 Pulling latest images from registry..." +docker-compose -f docker-compose.registry.yml pull + +echo "🚀 Starting containers with registry images..." +docker-compose -f docker-compose.registry.yml up -d + +echo "⏳ Waiting for services to start..." +sleep 30 + +echo "🔍 Checking service health..." +HEALTHY_SERVICES=$(docker-compose -f docker-compose.registry.yml ps --filter "status=running" --format "table {{.Service}}\t{{.Status}}" | grep -c "Up" || true) + +if [ "$HEALTHY_SERVICES" -ge 3 ]; then + echo "✅ Deployment successful!" + echo "🌐 Application should be available at: $CORS_ORIGIN" + echo "" + echo "📊 Service Status:" + docker-compose -f docker-compose.registry.yml ps + echo "" + echo "🏷️ Image Information:" + echo "Backend: ${BACKEND_IMAGE:-ghcr.io/your-username/rezepte-klaus-backend:latest}" + echo "Frontend: ${FRONTEND_IMAGE:-ghcr.io/your-username/rezepte-klaus-frontend:latest}" +else + echo "❌ Deployment failed! Check logs:" + docker-compose -f docker-compose.registry.yml logs --tail=50 + exit 1 +fi + +echo "" +echo "📋 Useful commands:" +echo " View logs: docker-compose -f docker-compose.registry.yml logs -f" +echo " Update: docker-compose -f docker-compose.registry.yml pull && docker-compose -f docker-compose.registry.yml up -d" +echo " Stop: docker-compose -f docker-compose.registry.yml down" \ No newline at end of file diff --git a/deploy-traefik.sh b/deploy-traefik.sh new file mode 100755 index 0000000..6282371 --- /dev/null +++ b/deploy-traefik.sh @@ -0,0 +1,91 @@ +#!/bin/bash +set -e + +echo "🚀 Deploying Rezepte Klaus with Traefik Proxy..." + +# Check if .env.production exists +if [ ! -f .env.production ]; then + echo "❌ Error: .env.production file not found!" + echo "Please copy .env.traefik.example to .env.production and configure it." + exit 1 +fi + +# Load environment variables +export $(cat .env.production | grep -v '^#' | xargs) + +# Validate required environment variables +if [ -z "$MYSQL_PASSWORD" ] || [ -z "$DOMAIN" ] || [ -z "$ACME_EMAIL" ]; then + echo "❌ Error: Required environment variables not set in .env.production" + echo "Please configure MYSQL_PASSWORD, DOMAIN, and ACME_EMAIL" + exit 1 +fi + +# Login to CitySensor registry if credentials are provided +if [ -n "$DOCKER_USERNAME" ] && [ -n "$DOCKER_PASSWORD" ] && [ -n "$DOCKER_REGISTRY" ]; then + echo "🔐 Logging into CitySensor registry..." + echo "$DOCKER_PASSWORD" | docker login "$DOCKER_REGISTRY" -u "$DOCKER_USERNAME" --password-stdin +fi + +# Check if required SQL files exist +REQUIRED_FILES=("Rezepte.sql" "ingredients.sql" "Zubereitung.sql" "rezepte_bilder.sql") +for file in "${REQUIRED_FILES[@]}"; do + if [ ! -f "$file" ]; then + echo "❌ Error: Required SQL file $file not found!" + echo "Please ensure all SQL files are present in the current directory." + exit 1 + fi +done + +# Create acme.json with correct permissions for Let's Encrypt +if [ ! -f ./acme.json ]; then + echo "🔒 Creating acme.json for Let's Encrypt..." + touch ./acme.json + chmod 600 ./acme.json +fi + +echo "🛑 Stopping existing containers..." +docker-compose -f docker-compose.traefik.yml down + +echo "📥 Pulling latest images from registry..." +docker-compose -f docker-compose.traefik.yml pull backend frontend + +echo "🚀 Starting containers with Traefik proxy..." +docker-compose -f docker-compose.traefik.yml up -d + +echo "⏳ Waiting for services to start..." +sleep 45 + +echo "🔍 Checking service health..." +HEALTHY_SERVICES=$(docker-compose -f docker-compose.traefik.yml ps --filter "status=running" --format "table {{.Service}}\t{{.Status}}" | grep -c "Up" || true) + +if [ "$HEALTHY_SERVICES" -ge 6 ]; then + echo "✅ Deployment successful!" + echo "" + echo "🌐 Your application is available at:" + echo " Main App: https://rezepte.$DOMAIN" + echo " phpMyAdmin: https://phpmyadmin.$DOMAIN" + echo " Portainer: https://portainer.$DOMAIN" + echo " Traefik Dashboard: https://traefik.$DOMAIN (admin/admin - please change!)" + echo "" + echo "📊 Service Status:" + docker-compose -f docker-compose.traefik.yml ps + echo "" + echo "🏷️ Image Information:" + echo "Backend: ${BACKEND_IMAGE:-ghcr.io/your-username/rezepte-klaus-backend:latest}" + echo "Frontend: ${FRONTEND_IMAGE:-ghcr.io/your-username/rezepte-klaus-frontend:latest}" + echo "" + echo "🔒 SSL Certificates:" + echo "Traefik will automatically request Let's Encrypt certificates." + echo "This may take a few minutes on first deployment." +else + echo "❌ Deployment failed! Check logs:" + docker-compose -f docker-compose.traefik.yml logs --tail=50 + exit 1 +fi + +echo "" +echo "📋 Useful commands:" +echo " View logs: docker-compose -f docker-compose.traefik.yml logs -f" +echo " Update: docker-compose -f docker-compose.traefik.yml pull && docker-compose -f docker-compose.traefik.yml up -d" +echo " Stop: docker-compose -f docker-compose.traefik.yml down" +echo " View Traefik logs: docker-compose -f docker-compose.traefik.yml logs traefik" \ No newline at end of file diff --git a/docker-compose.local-network.yml b/docker-compose.local-network.yml new file mode 100644 index 0000000..d838679 --- /dev/null +++ b/docker-compose.local-network.yml @@ -0,0 +1,103 @@ +version: '3.8' + +services: + # MySQL Database + mysql: + image: mysql:8.0 + container_name: rezepte-mysql + environment: + MYSQL_ROOT_PASSWORD: rootpassword + MYSQL_DATABASE: rezepte_klaus + MYSQL_USER: rezepte_user + MYSQL_PASSWORD: rezepte_pass + ports: + - "0.0.0.0:3307:3306" # Bind to all interfaces + volumes: + - mysql_data:/var/lib/mysql + - ./sql-init:/docker-entrypoint-initdb.d + networks: + - rezepte-network + restart: unless-stopped + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + timeout: 20s + retries: 10 + + # Backend API + backend: + build: + context: ./nodejs-version/backend + dockerfile: Dockerfile + container_name: rezepte-backend + environment: + NODE_ENV: production + PORT: 3001 + DATABASE_URL: mysql://rezepte_user:rezepte_pass@mysql:3306/rezepte_klaus + JWT_SECRET: your-super-secret-jwt-key-change-in-production + UPLOAD_PATH: /app/uploads + MAX_FILE_SIZE: 5242880 + # Allow access from any IP in local network + CORS_ORIGIN: "*" + ports: + - "0.0.0.0:3001:3001" # Bind to all interfaces + volumes: + - uploads_data:/app/uploads + - ./uploads:/app/legacy-uploads:ro # Mount existing uploads as read-only + networks: + - rezepte-network + depends_on: + mysql: + condition: service_healthy + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3001/api/health"] + interval: 30s + timeout: 10s + retries: 3 + + # Frontend Application + frontend: + build: + context: ./nodejs-version/frontend + dockerfile: Dockerfile + args: + # Use host IP instead of localhost for API calls + VITE_API_URL: http://${HOST_IP:-192.168.1.100}:3001/api + container_name: rezepte-frontend + ports: + - "0.0.0.0:3000:80" # Bind to all interfaces + networks: + - rezepte-network + depends_on: + - backend + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:80"] + interval: 30s + timeout: 10s + retries: 3 + + # Legacy PHP Application (optional) + php-app: + build: . + container_name: rezepte-php-legacy + ports: + - "0.0.0.0:8080:80" # Bind to all interfaces + volumes: + - ./uploads:/var/www/html/uploads + - .:/var/www/html + networks: + - rezepte-network + depends_on: + - mysql + restart: unless-stopped + +volumes: + mysql_data: + driver: local + uploads_data: + driver: local + +networks: + rezepte-network: + driver: bridge \ No newline at end of file diff --git a/docker-compose.modern.yml b/docker-compose.modern.yml new file mode 100644 index 0000000..d0c73e6 --- /dev/null +++ b/docker-compose.modern.yml @@ -0,0 +1,124 @@ +services: + # MySQL Database + mysql: + image: mysql:8.0 + container_name: rezepte-mysql + environment: + MYSQL_ROOT_PASSWORD: rootpassword + MYSQL_DATABASE: rezepte_klaus + MYSQL_USER: rezepte_user + MYSQL_PASSWORD: rezepte_pass + ports: + - "3307:3306" + volumes: + - mysql_data:/var/lib/mysql + - ./sql-init:/docker-entrypoint-initdb.d + networks: + - rezepte-network + restart: unless-stopped + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + timeout: 20s + retries: 10 + + # Backend API + backend: + build: + context: ./nodejs-version/backend + dockerfile: Dockerfile + container_name: rezepte-backend + environment: + NODE_ENV: production + PORT: 3001 + DATABASE_URL: mysql://rezepte_user:rezepte_pass@mysql:3306/rezepte_klaus + JWT_SECRET: your-super-secret-jwt-key-change-in-production + UPLOAD_PATH: /app/uploads + MAX_FILE_SIZE: 5242880 + CORS_ORIGIN: http://localhost:3000 + ports: + - "3001:3001" + volumes: + - uploads_data:/app/uploads + - ./uploads:/app/legacy-uploads:ro # Mount existing uploads as read-only + networks: + - rezepte-network + depends_on: + mysql: + condition: service_healthy + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3001/api/health"] + interval: 30s + timeout: 10s + retries: 3 + + # Frontend Application + frontend: + build: + context: ./nodejs-version/frontend + dockerfile: Dockerfile + args: + VITE_API_URL: http://localhost:3001/api + container_name: rezepte-frontend + ports: + - "3000:80" + networks: + - rezepte-network + depends_on: + - backend + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:80"] + interval: 30s + timeout: 10s + retries: 3 + + # Legacy PHP Application (optional) + php-app: + build: . + container_name: rezepte-php-legacy + ports: + - "8082:80" + volumes: + - .:/var/www/html + depends_on: + - mysql + networks: + - rezepte-network + environment: + DB_HOST: mysql + DB_NAME: rezepte_klaus + DB_USER: rezepte_user + DB_PASS: rezepte_pass + profiles: + - legacy + + # phpMyAdmin + phpmyadmin: + image: phpmyadmin:latest + container_name: rezepte-phpmyadmin + ports: + - "8083:80" + environment: + PMA_HOST: mysql + PMA_USER: rezepte_user + PMA_PASSWORD: rezepte_pass + MYSQL_ROOT_PASSWORD: rootpassword + depends_on: + - mysql + networks: + - rezepte-network + profiles: + - admin + +# Networks +networks: + rezepte-network: + driver: bridge + +# Volumes for persistent data +volumes: + mysql_data: + driver: local + uploads_data: + driver: local \ No newline at end of file diff --git a/docker-compose.production.yml b/docker-compose.production.yml new file mode 100644 index 0000000..a1224a5 --- /dev/null +++ b/docker-compose.production.yml @@ -0,0 +1,88 @@ +version: '3.8' + +services: + mysql: + image: mysql:8.0 + container_name: rezepte-mysql-prod + restart: unless-stopped + environment: + MYSQL_DATABASE: rezepte_klaus + 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 + - ./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 + # Expose port only for debugging - remove in production + # ports: + # - "3306:3306" + + backend: + build: + context: ./nodejs-version/backend + dockerfile: Dockerfile + 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_klaus + - JWT_SECRET=${JWT_SECRET:-change_this_jwt_secret_min_32_characters} + - CORS_ORIGIN=${CORS_ORIGIN:-http://localhost} + - PORT=3001 + volumes: + - uploads_data:/app/uploads + - ./upload:/app/legacy-uploads:ro + 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 + # Expose port only if not using reverse proxy + ports: + - "3001:3001" + + frontend: + build: + context: ./nodejs-version/frontend + dockerfile: Dockerfile + args: + - VITE_API_BASE_URL=${API_BASE_URL:-http://localhost:3001/api} + container_name: rezepte-frontend-prod + restart: unless-stopped + ports: + - "80:80" + # Add port 443 if handling SSL directly in container + # - "443:443" + 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 \ No newline at end of file diff --git a/docker-compose.registry.yml b/docker-compose.registry.yml new file mode 100644 index 0000000..8bbffca --- /dev/null +++ b/docker-compose.registry.yml @@ -0,0 +1,80 @@ +version: '3.8' + +services: + mysql: + image: mysql:8.0 + container_name: rezepte-mysql-prod + restart: unless-stopped + environment: + MYSQL_DATABASE: rezepte_klaus + 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 + # SQL files must be present on server + - ./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: + # Use pre-built image from registry instead of building + image: ${BACKEND_IMAGE:-ghcr.io/your-username/rezepte-klaus-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_klaus + - JWT_SECRET=${JWT_SECRET:-change_this_jwt_secret_min_32_characters} + - CORS_ORIGIN=${CORS_ORIGIN:-http://localhost} + - PORT=3001 + volumes: + - uploads_data:/app/uploads + # Legacy uploads can be mounted if needed + # - ./legacy-uploads:/app/legacy-uploads:ro + 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 + ports: + - "3001:3001" + + frontend: + # Use pre-built image from registry instead of building + image: ${FRONTEND_IMAGE:-ghcr.io/your-username/rezepte-klaus-frontend:latest} + container_name: rezepte-frontend-prod + restart: unless-stopped + ports: + - "80:80" + 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 \ No newline at end of file diff --git a/docker-compose.traefik-external-db.yml b/docker-compose.traefik-external-db.yml new file mode 100644 index 0000000..6162912 --- /dev/null +++ b/docker-compose.traefik-external-db.yml @@ -0,0 +1,175 @@ +version: '3.8' + +services: + traefik: + image: traefik:v3.0 + container_name: traefik + restart: unless-stopped + command: + # API und Dashboard + - --api.dashboard=true + - --api.insecure=false + # Entrypoints + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + # Docker Provider + - --providers.docker=true + - --providers.docker.exposedbydefault=false + # Let's Encrypt + - --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL} + - --certificatesresolvers.letsencrypt.acme.storage=/acme.json + - --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web + # Logging + - --log.level=INFO + - --accesslog=true + # Global HTTP -> HTTPS redirect + - --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: + # Dashboard + - "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 für Dashboard (admin:admin - bitte ändern!) + - "traefik.http.middlewares.auth.basicauth.users=admin:$$2y$$10$$8eO9J8Ef.LswB5K4l1.ZJ.qZBOa6ZXJ3X2y3zCZLCr9zHVJ8vJ2Ga" + networks: + - traefik-network + # Connect to external MySQL network + - ${EXTERNAL_MYSQL_NETWORK:-gitea_default} + + backend: + # Use pre-built image from registry instead of building + image: ${BACKEND_IMAGE:-ghcr.io/your-username/rezepte-klaus-backend:latest} + container_name: rezepte-backend-prod + restart: unless-stopped + environment: + - NODE_ENV=production + - DATABASE_URL=mysql://rezepte_user:${MYSQL_REZEPTE_PASSWORD}@${MYSQL_HOST:-mysql}:${MYSQL_PORT:-3306}/rezepte_klaus + - CORS_ORIGIN=https://rezepte.${DOMAIN} + - PORT=3001 + volumes: + - uploads_data:/app/uploads + # Legacy uploads can be mounted if needed + # - ./legacy-uploads:/app/legacy-uploads:ro + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3001/api/health"] + interval: 30s + timeout: 10s + retries: 3 + labels: + - "traefik.enable=true" + # API Routes + - "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" + # Upload Routes + - "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" + networks: + - traefik-network + # Connect to external MySQL network + - ${EXTERNAL_MYSQL_NETWORK:-gitea_default} + + frontend: + # Use pre-built image from registry instead of building + image: ${FRONTEND_IMAGE:-ghcr.io/your-username/rezepte-klaus-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 + labels: + - "traefik.enable=true" + # Frontend Routes (catch-all) + - "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" + # Lower priority than backend routes + - "traefik.http.routers.frontend.priority=1" + - "traefik.http.routers.backend.priority=10" + - "traefik.http.routers.backend-uploads.priority=10" + networks: + - traefik-network + + phpmyadmin: + image: phpmyadmin/phpmyadmin:latest + container_name: rezepte-phpmyadmin + restart: unless-stopped + environment: + - PMA_HOST=${MYSQL_HOST:-mysql} + - PMA_PORT=${MYSQL_PORT:-3306} + - PMA_USER=${MYSQL_ADMIN_USER:-root} + - PMA_PASSWORD=${MYSQL_ADMIN_PASSWORD} + - UPLOAD_LIMIT=2G + - MEMORY_LIMIT=2G + - MAX_EXECUTION_TIME=0 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/"] + interval: 30s + timeout: 10s + retries: 3 + 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" + # Optional: Add basic auth for extra security + # - "traefik.http.routers.phpmyadmin.middlewares=auth" + networks: + - traefik-network + # Connect to external MySQL network + - ${EXTERNAL_MYSQL_NETWORK:-gitea_default} + + portainer: + image: portainer/portainer-ce:latest + container_name: portainer + restart: unless-stopped + command: -H unix:///var/run/docker.sock + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - portainer_data:/data + labels: + - "traefik.enable=true" + - "traefik.http.routers.portainer.rule=Host(`portainer.${DOMAIN}`)" + - "traefik.http.routers.portainer.entrypoints=websecure" + - "traefik.http.routers.portainer.tls.certresolver=letsencrypt" + - "traefik.http.services.portainer.loadbalancer.server.port=9000" + # Optional: Add basic auth for extra security + # - "traefik.http.routers.portainer.middlewares=auth" + networks: + - traefik-network + +volumes: + uploads_data: + driver: local + traefik_acme: + driver: local + portainer_data: + driver: local + +networks: + traefik-network: + driver: bridge + # Reference to external network (will be created by Gitea) + # This network should already exist from your Gitea installation + gitea_default: + external: true \ No newline at end of file diff --git a/docker-compose.traefik.yml b/docker-compose.traefik.yml new file mode 100644 index 0000000..8ced8a0 --- /dev/null +++ b/docker-compose.traefik.yml @@ -0,0 +1,202 @@ +version: '3.8' + +services: + traefik: + image: traefik:v3.0 + container_name: traefik + restart: unless-stopped + command: + # API und Dashboard + - --api.dashboard=true + - --api.insecure=false + # Entrypoints + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + # Docker Provider + - --providers.docker=true + - --providers.docker.exposedbydefault=false + # Let's Encrypt + - --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL} + - --certificatesresolvers.letsencrypt.acme.storage=/acme.json + - --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web + # Logging + - --log.level=INFO + - --accesslog=true + # Global HTTP -> HTTPS redirect + - --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: + # Dashboard + - "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 für Dashboard (admin:admin - bitte ändern!) + - "traefik.http.middlewares.auth.basicauth.users=admin:$$2y$$10$$8eO9J8Ef.LswB5K4l1.ZJ.qZBOa6ZXJ3X2y3zCZLCr9zHVJ8vJ2Ga" + networks: + - traefik-network + - rezepte-network + + mysql: + image: mysql:8.0 + container_name: rezepte-mysql-prod + restart: unless-stopped + environment: + MYSQL_DATABASE: rezepte_klaus + 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 + # SQL files must be present on server + - ./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: + # Use pre-built image from registry instead of building + image: ${BACKEND_IMAGE:-ghcr.io/your-username/rezepte-klaus-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_klaus + - CORS_ORIGIN=https://rezepte.${DOMAIN} + - PORT=3001 + volumes: + - uploads_data:/app/uploads + # Legacy uploads can be mounted if needed + # - ./legacy-uploads:/app/legacy-uploads:ro + depends_on: + mysql: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3001/api/health"] + interval: 30s + timeout: 10s + retries: 3 + labels: + - "traefik.enable=true" + # API Routes + - "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" + # Upload Routes + - "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" + networks: + - traefik-network + - rezepte-network + + frontend: + # Use pre-built image from registry instead of building + image: ${FRONTEND_IMAGE:-ghcr.io/your-username/rezepte-klaus-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 + labels: + - "traefik.enable=true" + # Frontend Routes (catch-all) + - "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" + # Lower priority than backend routes + - "traefik.http.routers.frontend.priority=1" + - "traefik.http.routers.backend.priority=10" + - "traefik.http.routers.backend-uploads.priority=10" + networks: + - traefik-network + + phpmyadmin: + image: phpmyadmin/phpmyadmin:latest + container_name: rezepte-phpmyadmin + restart: unless-stopped + environment: + - PMA_HOST=mysql + - PMA_PORT=3306 + - PMA_USER=root + - PMA_PASSWORD=${MYSQL_ROOT_PASSWORD} + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} + - UPLOAD_LIMIT=2G + - MEMORY_LIMIT=2G + - MAX_EXECUTION_TIME=0 + depends_on: + mysql: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/"] + interval: 30s + timeout: 10s + retries: 3 + 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" + # Optional: Add basic auth for extra security + # - "traefik.http.routers.phpmyadmin.middlewares=auth" + networks: + - traefik-network + - rezepte-network + + portainer: + image: portainer/portainer-ce:latest + container_name: portainer + restart: unless-stopped + command: -H unix:///var/run/docker.sock + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - portainer_data:/data + labels: + - "traefik.enable=true" + - "traefik.http.routers.portainer.rule=Host(`portainer.${DOMAIN}`)" + - "traefik.http.routers.portainer.entrypoints=websecure" + - "traefik.http.routers.portainer.tls.certresolver=letsencrypt" + - "traefik.http.services.portainer.loadbalancer.server.port=9000" + # Optional: Add basic auth for extra security + # - "traefik.http.routers.portainer.middlewares=auth" + networks: + - traefik-network + +volumes: + mysql_data: + driver: local + uploads_data: + driver: local + traefik_acme: + driver: local + portainer_data: + driver: local + +networks: + traefik-network: + driver: bridge + rezepte-network: + driver: bridge \ No newline at end of file diff --git a/docker-deploy.sh b/docker-deploy.sh new file mode 100755 index 0000000..39a6e4c --- /dev/null +++ b/docker-deploy.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +# Farben für Output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +echo -e "${GREEN}Rezepte Klaus - Docker Build & Deploy Script${NC}" +echo "==================================================" + +# Überprüfe ob Docker läuft +if ! docker info > /dev/null 2>&1; then + echo -e "${RED}❌ Docker ist nicht verfügbar. Bitte starte Docker Desktop.${NC}" + exit 1 +fi + +# Environment-Datei kopieren +if [ ! -f .env ]; then + echo -e "${YELLOW}📝 Kopiere .env.docker zu .env${NC}" + cp .env.docker .env +else + echo -e "${GREEN}✅ .env Datei bereits vorhanden${NC}" +fi + +# Erstelle Upload-Ordner falls nicht vorhanden +echo -e "${YELLOW}📁 Erstelle Upload-Ordner...${NC}" +mkdir -p docker-data/uploads +mkdir -p docker-data/mysql + +# Legacy Uploads kopieren falls vorhanden +if [ -d "upload" ]; then + echo -e "${YELLOW}📋 Kopiere bestehende Uploads...${NC}" + cp -r upload/* docker-data/uploads/ 2>/dev/null || true +fi + +# Stoppe eventuell laufende Container +echo -e "${YELLOW}🛑 Stoppe laufende Container...${NC}" +docker-compose -f docker-compose.modern.yml down + +# Entferne alte Images (optional - auskommentiert) +# echo -e "${YELLOW}🗑️ Entferne alte Images...${NC}" +# docker-compose -f docker-compose.modern.yml down --rmi all + +# Build alle Services +echo -e "${YELLOW}🔨 Baue alle Services...${NC}" +docker-compose -f docker-compose.modern.yml build --no-cache + +# Starte Services +echo -e "${YELLOW}🚀 Starte alle Services...${NC}" +docker-compose -f docker-compose.modern.yml up -d + +# Warte auf MySQL +echo -e "${YELLOW}⏳ Warte auf MySQL...${NC}" +until docker-compose -f docker-compose.modern.yml exec mysql mysqladmin ping -h"localhost" --silent; do + echo -n "." + sleep 2 +done + +echo -e "${GREEN}✅ MySQL ist bereit${NC}" + +# Führe Database Migrations aus +echo -e "${YELLOW}🗃️ Führe Database Migrations aus...${NC}" +docker-compose -f docker-compose.modern.yml exec backend npx prisma migrate deploy || true +docker-compose -f docker-compose.modern.yml exec backend npx prisma generate || true + +# Zeige Container Status +echo -e "${GREEN}📊 Container Status:${NC}" +docker-compose -f docker-compose.modern.yml ps + +echo "" +echo -e "${GREEN}🎉 Deployment erfolgreich!${NC}" +echo "" +echo "Verfügbare Services:" +echo -e "🌐 Frontend: ${GREEN}http://localhost:3000${NC}" +echo -e "🔧 Backend API: ${GREEN}http://localhost:3001${NC}" +echo -e "🗃️ phpMyAdmin: ${GREEN}http://localhost:8080${NC}" +echo -e "📁 Legacy PHP: ${GREEN}http://localhost:8090${NC} (optional)" +echo "" +echo "Nützliche Commands:" +echo " docker-compose -f docker-compose.modern.yml logs -f # Logs anzeigen" +echo " docker-compose -f docker-compose.modern.yml down # Services stoppen" +echo " docker-compose -f docker-compose.modern.yml exec backend bash # Backend Shell" +echo "" \ No newline at end of file diff --git a/docker-stop.sh b/docker-stop.sh new file mode 100755 index 0000000..ad555ee --- /dev/null +++ b/docker-stop.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# Farben für Output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +echo -e "${GREEN}Rezepte Klaus - Docker Stop Script${NC}" +echo "===========================================" + +# Stoppe alle Services +echo -e "${YELLOW}🛑 Stoppe alle Docker Services...${NC}" +docker-compose -f docker-compose.modern.yml down + +# Zeige gestoppte Container +echo -e "${GREEN}📊 Container Status:${NC}" +docker-compose -f docker-compose.modern.yml ps + +echo -e "${GREEN}✅ Alle Services gestoppt${NC}" +echo "" +echo "Services können mit ./docker-deploy.sh wieder gestartet werden" \ No newline at end of file diff --git a/generate-jwt-secret.sh b/generate-jwt-secret.sh new file mode 100755 index 0000000..d165884 --- /dev/null +++ b/generate-jwt-secret.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +echo "🔐 JWT Secret Generator" +echo "========================" +echo "" + +# Method 1: OpenSSL (most secure) +if command -v openssl &> /dev/null; then + JWT_SECRET_OPENSSL=$(openssl rand -base64 32) + echo "Method 1 (OpenSSL - Recommended):" + echo "JWT_SECRET=$JWT_SECRET_OPENSSL" + echo "" +fi + +# Method 2: Node.js crypto +if command -v node &> /dev/null; then + JWT_SECRET_NODE=$(node -e "console.log(require('crypto').randomBytes(32).toString('base64'))") + echo "Method 2 (Node.js crypto):" + echo "JWT_SECRET=$JWT_SECRET_NODE" + echo "" +fi + +# Method 3: Python (if available) +if command -v python3 &> /dev/null; then + JWT_SECRET_PYTHON=$(python3 -c "import secrets, base64; print(base64.b64encode(secrets.token_bytes(32)).decode())") + echo "Method 3 (Python):" + echo "JWT_SECRET=$JWT_SECRET_PYTHON" + echo "" +fi + +# Method 4: Manual example +echo "Method 4 (Manual - change the values):" +echo "JWT_SECRET=MySuper$ecureJWT$ecret2025!Random123" +echo "" + +echo "💡 Tips:" +echo "- Use at least 32 characters" +echo "- Mix letters, numbers, and symbols" +echo "- Keep it secret and secure" +echo "- Never commit it to version control" +echo "" +echo "⚠️ Note: Your current app doesn't use JWT authentication yet." +echo " This is prepared for future authentication features." \ No newline at end of file diff --git a/nginx-rezepte-klaus.conf b/nginx-rezepte-klaus.conf new file mode 100644 index 0000000..8912ded --- /dev/null +++ b/nginx-rezepte-klaus.conf @@ -0,0 +1,137 @@ +# Nginx Configuration for Rezepte Klaus +# Place this in /etc/nginx/sites-available/rezepte-klaus +# Then: sudo ln -s /etc/nginx/sites-available/rezepte-klaus /etc/nginx/sites-enabled/ + +# Redirect HTTP to HTTPS +server { + listen 80; + server_name yourdomain.com www.yourdomain.com; + + # Let's Encrypt challenge location + location /.well-known/acme-challenge/ { + root /var/www/html; + } + + # Redirect all other traffic to HTTPS + location / { + return 301 https://$server_name$request_uri; + } +} + +# HTTPS Configuration +server { + listen 443 ssl http2; + server_name yourdomain.com www.yourdomain.com; + + # SSL Configuration + ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; + ssl_trusted_certificate /etc/letsencrypt/live/yourdomain.com/chain.pem; + + # SSL Security + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # Security Headers + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options DENY always; + add_header X-Content-Type-Options nosniff always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Gzip Compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/javascript + application/xml+rss + application/json; + + # Client upload size + client_max_body_size 10M; + + # Frontend (React App) + location / { + proxy_pass http://localhost:80; + 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; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + + # Handle WebSocket connections (if needed) + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # Timeouts + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # Backend API + location /api/ { + proxy_pass http://localhost:3001; + 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; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + + # CORS handling is done by the backend + proxy_hide_header Access-Control-Allow-Origin; + proxy_hide_header Access-Control-Allow-Methods; + proxy_hide_header Access-Control-Allow-Headers; + + # Timeouts + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # Static uploads serving + location /uploads/ { + proxy_pass http://localhost:3001; + 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; + + # Cache static assets + expires 1y; + add_header Cache-Control "public, immutable"; + + # Security for images + add_header X-Content-Type-Options nosniff; + } + + # Health check endpoint + location /health { + proxy_pass http://localhost:3001/api/health; + access_log off; + } + + # Block access to sensitive files + location ~ /\. { + deny all; + } + + location ~ /(package\.json|package-lock\.json|yarn\.lock|\.env|docker-compose\.yml)$ { + deny all; + } + + # Logging + access_log /var/log/nginx/rezepte-klaus.access.log; + error_log /var/log/nginx/rezepte-klaus.error.log; +} \ No newline at end of file diff --git a/nodejs-version/backend/Dockerfile b/nodejs-version/backend/Dockerfile new file mode 100644 index 0000000..ecb505b --- /dev/null +++ b/nodejs-version/backend/Dockerfile @@ -0,0 +1,89 @@ +# Backend Dockerfile +FROM node:18-alpine AS builder + +# Install OpenSSL for Prisma compatibility +RUN apk add --no-cache openssl openssl-dev + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci --only=production + +# Copy source code +COPY . . + +# Build the application +RUN npm run build + +# Production stage +FROM node:18-alpine AS production + +# Install required system dependencies for Prisma and health checks +RUN apk add --no-cache \ + curl \ + openssl \ + openssl-dev \ + libc6-compat \ + && rm -rf /var/cache/apk/* + +# Install curl for healthcheck +RUN apk add --no-cache curl + +# Create app user +RUN addgroup -g 1001 -S nodejs +RUN adduser -S backend -u 1001 + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install only production dependencies +RUN npm ci --only=production && npm cache clean --force + +# Copy built application from builder stage +COPY --from=builder /app/dist ./dist + +# Copy prisma schema for runtime +COPY --from=builder /app/prisma ./prisma + +# Create uploads directory +RUN mkdir -p uploads legacy-uploads && chown -R backend:nodejs uploads legacy-uploads + +# Create migration script for legacy uploads (via volumes) +COPY </dev/null || true + chown -R backend:nodejs /app/uploads + echo "Upload migration completed." +else + echo "No legacy uploads found to migrate." +fi +EOF + +RUN chmod +x ./migrate-uploads.sh + +# Generate Prisma client +RUN npx prisma generate + +# Switch to non-root user +USER backend + +# Expose port +EXPOSE 3001 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:3001/api/health || exit 1 + +# Start the application +CMD ["sh", "-c", "./migrate-uploads.sh && node dist/app.js"] \ No newline at end of file diff --git a/nodejs-version/backend/dist/app.d.ts.map b/nodejs-version/backend/dist/app.d.ts.map index 2c20394..8f69e50 100644 --- a/nodejs-version/backend/dist/app.d.ts.map +++ b/nodejs-version/backend/dist/app.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAgBA,QAAA,MAAM,GAAG,6CAAY,CAAC;AAkGtB,eAAe,GAAG,CAAC"} \ No newline at end of file +{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAgBA,QAAA,MAAM,GAAG,6CAAY,CAAC;AA2GtB,eAAe,GAAG,CAAC"} \ No newline at end of file diff --git a/nodejs-version/backend/dist/app.js b/nodejs-version/backend/dist/app.js index 863dee6..48590ef 100644 --- a/nodejs-version/backend/dist/app.js +++ b/nodejs-version/backend/dist/app.js @@ -27,12 +27,20 @@ const limiter = (0, express_rate_limit_1.default)({ message: 'Too many requests from this IP, please try again later.', }); app.use(limiter); +const allowedOrigins = [ + 'http://localhost:5173', + 'http://localhost:3000', + config_1.config.cors.origin +].filter(Boolean); app.use((0, cors_1.default)({ - origin: config_1.config.cors.origin, + origin: allowedOrigins, credentials: true, })); app.use((req, res, next) => { - res.header('Access-Control-Allow-Origin', 'http://localhost:5173'); + const origin = req.headers.origin; + if (origin && allowedOrigins.includes(origin)) { + res.header('Access-Control-Allow-Origin', origin); + } res.header('Access-Control-Allow-Credentials', 'true'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); diff --git a/nodejs-version/backend/dist/app.js.map b/nodejs-version/backend/dist/app.js.map index 7db2b91..95dbcc7 100644 --- a/nodejs-version/backend/dist/app.js.map +++ b/nodejs-version/backend/dist/app.js.map @@ -1 +1 @@ -{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/nodejs-version/backend/dist/routes/images.d.ts.map b/nodejs-version/backend/dist/routes/images.d.ts.map index f925a76..47bf143 100644 --- a/nodejs-version/backend/dist/routes/images.d.ts.map +++ b/nodejs-version/backend/dist/routes/images.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"images.d.ts","sourceRoot":"","sources":["../../src/routes/images.ts"],"names":[],"mappings":"AAKA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAuGxB,eAAe,MAAM,CAAC"} \ No newline at end of file +{"version":3,"file":"images.d.ts","sourceRoot":"","sources":["../../src/routes/images.ts"],"names":[],"mappings":"AAOA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAqQxB,eAAe,MAAM,CAAC"} \ No newline at end of file diff --git a/nodejs-version/backend/dist/routes/images.js b/nodejs-version/backend/dist/routes/images.js index 89eac38..74a394c 100644 --- a/nodejs-version/backend/dist/routes/images.js +++ b/nodejs-version/backend/dist/routes/images.js @@ -5,10 +5,140 @@ var __importDefault = (this && this.__importDefault) || function (mod) { Object.defineProperty(exports, "__esModule", { value: true }); const express_1 = require("express"); const client_1 = require("@prisma/client"); +const multer_1 = __importDefault(require("multer")); const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); +const config_1 = require("../config/config"); const router = (0, express_1.Router)(); const prisma = new client_1.PrismaClient(); +const storage = multer_1.default.diskStorage({ + destination: (req, file, cb) => { + const recipeNumber = req.body.recipeNumber || req.params.recipeNumber; + if (!recipeNumber) { + return cb(new Error('Recipe number is required'), ''); + } + const uploadDir = path_1.default.join(process.cwd(), '../../uploads', recipeNumber); + if (!fs_1.default.existsSync(uploadDir)) { + fs_1.default.mkdirSync(uploadDir, { recursive: true }); + } + cb(null, uploadDir); + }, + filename: (req, file, cb) => { + const recipeNumber = req.body.recipeNumber || req.params.recipeNumber; + if (!recipeNumber) { + return cb(new Error('Recipe number is required'), ''); + } + const uploadDir = path_1.default.join(process.cwd(), '../../uploads', recipeNumber); + const existingFiles = fs_1.default.existsSync(uploadDir) + ? fs_1.default.readdirSync(uploadDir).filter(f => f.match(new RegExp(`^${recipeNumber}_\\d+\\.jpg$`))) + : []; + const nextIndex = existingFiles.length; + const filename = `${recipeNumber}_${nextIndex}.jpg`; + cb(null, filename); + } +}); +const upload = (0, multer_1.default)({ + storage, + limits: { + fileSize: config_1.config.upload.maxFileSize, + }, + fileFilter: (req, file, cb) => { + const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; + if (allowedTypes.includes(file.mimetype)) { + cb(null, true); + } + else { + cb(new Error('Invalid file type. Only JPEG, PNG and WebP are allowed.')); + } + }, +}); +router.post('/upload/:recipeId', upload.array('images', 10), async (req, res, next) => { + try { + const { recipeId } = req.params; + const files = req.files; + if (!recipeId) { + return res.status(400).json({ + success: false, + message: 'Recipe ID is required', + }); + } + if (!files || files.length === 0) { + return res.status(400).json({ + success: false, + message: 'No files uploaded', + }); + } + const recipe = await prisma.recipe.findUnique({ + where: { id: parseInt(recipeId) } + }); + if (!recipe) { + return res.status(404).json({ + success: false, + message: 'Recipe not found', + }); + } + const imagePromises = files.map(file => { + const relativePath = `uploads/${recipe.recipeNumber}/${file.filename}`; + return prisma.recipeImage.create({ + data: { + recipeId: parseInt(recipeId), + filePath: relativePath, + } + }); + }); + const images = await Promise.all(imagePromises); + return res.status(201).json({ + success: true, + data: images, + message: `${files.length} images uploaded successfully`, + }); + } + catch (error) { + if (req.files) { + const files = req.files; + files.forEach(file => { + if (fs_1.default.existsSync(file.path)) { + fs_1.default.unlinkSync(file.path); + } + }); + } + next(error); + } +}); +router.delete('/:id', async (req, res, next) => { + try { + const { id } = req.params; + if (!id) { + return res.status(400).json({ + success: false, + message: 'Image ID is required', + }); + } + const image = await prisma.recipeImage.findUnique({ + where: { id: parseInt(id) } + }); + if (!image) { + return res.status(404).json({ + success: false, + message: 'Image not found', + }); + } + const fullPath = path_1.default.join(process.cwd(), '../..', image.filePath); + if (fs_1.default.existsSync(fullPath)) { + fs_1.default.unlinkSync(fullPath); + } + await prisma.recipeImage.delete({ + where: { id: parseInt(id) } + }); + return res.json({ + success: true, + message: 'Image deleted successfully', + }); + } + catch (error) { + next(error); + } +}); router.get('/recipe/:recipeId', async (req, res, next) => { try { const { recipeId } = req.params; diff --git a/nodejs-version/backend/dist/routes/images.js.map b/nodejs-version/backend/dist/routes/images.js.map index 82206cb..ffaf549 100644 --- a/nodejs-version/backend/dist/routes/images.js.map +++ b/nodejs-version/backend/dist/routes/images.js.map @@ -1 +1 @@ -{"version":3,"file":"images.js","sourceRoot":"","sources":["../../src/routes/images.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"images.js","sourceRoot":"","sources":["../../src/routes/images.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/nodejs-version/backend/dist/routes/recipes.d.ts.map b/nodejs-version/backend/dist/routes/recipes.d.ts.map index 2ea2a33..9eeebde 100644 --- a/nodejs-version/backend/dist/routes/recipes.d.ts.map +++ b/nodejs-version/backend/dist/routes/recipes.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"recipes.d.ts","sourceRoot":"","sources":["../../src/routes/recipes.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA4PxB,eAAe,MAAM,CAAC"} \ No newline at end of file +{"version":3,"file":"recipes.d.ts","sourceRoot":"","sources":["../../src/routes/recipes.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAoQxB,eAAe,MAAM,CAAC"} \ No newline at end of file diff --git a/nodejs-version/backend/dist/routes/recipes.js b/nodejs-version/backend/dist/routes/recipes.js index 88f4ec4..bdfa36f 100644 --- a/nodejs-version/backend/dist/routes/recipes.js +++ b/nodejs-version/backend/dist/routes/recipes.js @@ -74,8 +74,15 @@ router.get('/:id', async (req, res, next) => { message: 'Recipe ID is required', }); } + const recipeId = parseInt(id); + if (isNaN(recipeId)) { + return res.status(400).json({ + success: false, + message: 'Invalid recipe ID format', + }); + } const recipe = await prisma.recipe.findUnique({ - where: { id: parseInt(id) }, + where: { id: recipeId }, include: { images: true, ingredientsList: true, diff --git a/nodejs-version/backend/dist/routes/recipes.js.map b/nodejs-version/backend/dist/routes/recipes.js.map index 3b34099..43ee7a7 100644 --- a/nodejs-version/backend/dist/routes/recipes.js.map +++ b/nodejs-version/backend/dist/routes/recipes.js.map @@ -1 +1 @@ -{"version":3,"file":"recipes.js","sourceRoot":"","sources":["../../src/routes/recipes.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"recipes.js","sourceRoot":"","sources":["../../src/routes/recipes.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/nodejs-version/backend/package-lock.json b/nodejs-version/backend/package-lock.json index b671d19..aecb76f 100644 --- a/nodejs-version/backend/package-lock.json +++ b/nodejs-version/backend/package-lock.json @@ -28,7 +28,7 @@ "@types/express": "^4.17.21", "@types/jest": "^29.5.8", "@types/jsonwebtoken": "^9.0.5", - "@types/multer": "^1.4.11", + "@types/multer": "^1.4.13", "@types/node": "^20.8.10", "@typescript-eslint/eslint-plugin": "^6.9.1", "@typescript-eslint/parser": "^6.9.1", diff --git a/nodejs-version/backend/package.json b/nodejs-version/backend/package.json index a74a354..81ad3b0 100644 --- a/nodejs-version/backend/package.json +++ b/nodejs-version/backend/package.json @@ -34,7 +34,7 @@ "@types/express": "^4.17.21", "@types/jest": "^29.5.8", "@types/jsonwebtoken": "^9.0.5", - "@types/multer": "^1.4.11", + "@types/multer": "^1.4.13", "@types/node": "^20.8.10", "@typescript-eslint/eslint-plugin": "^6.9.1", "@typescript-eslint/parser": "^6.9.1", diff --git a/nodejs-version/backend/src/app.ts b/nodejs-version/backend/src/app.ts index 42bfc78..4039119 100644 --- a/nodejs-version/backend/src/app.ts +++ b/nodejs-version/backend/src/app.ts @@ -30,15 +30,36 @@ const limiter = rateLimit({ }); app.use(limiter); -// CORS configuration -app.use(cors({ - origin: config.cors.origin, - credentials: true, -})); +// CORS configuration - Allow both development and production origins +const allowedOrigins = [ + 'http://localhost:5173', // Vite dev server + 'http://localhost:3000', // Docker frontend + config.cors.origin // Environment configured origin +].filter(Boolean); + +// Add local network origins if CORS_ORIGIN is "*" (for local network access) +const corsConfig = config.cors.origin === '*' + ? { + origin: true, // Allow all origins for local network + credentials: true, + } + : { + origin: allowedOrigins, + credentials: true, + }; + +app.use(cors(corsConfig)); // Additional CORS headers for all requests app.use((req, res, next) => { - res.header('Access-Control-Allow-Origin', 'http://localhost:5173'); + const origin = req.headers.origin; + + if (config.cors.origin === '*') { + // Allow all origins for local network access + res.header('Access-Control-Allow-Origin', origin || '*'); + } else if (origin && allowedOrigins.includes(origin)) { + res.header('Access-Control-Allow-Origin', origin); + } res.header('Access-Control-Allow-Credentials', 'true'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); @@ -56,6 +77,9 @@ app.use(express.urlencoded({ extended: true, limit: '10mb' })); // Request logging app.use(requestLogger); +// Static file serving for uploads +app.use('/uploads', express.static(path.join(process.cwd(), 'uploads'))); + // API routes app.use('/api/health', healthRoutes); app.use('/api/recipes', recipeRoutes); diff --git a/nodejs-version/backend/src/routes/images.ts b/nodejs-version/backend/src/routes/images.ts index 32c352e..9fa9378 100644 --- a/nodejs-version/backend/src/routes/images.ts +++ b/nodejs-version/backend/src/routes/images.ts @@ -1,11 +1,180 @@ import { Router, Request, Response, NextFunction } from 'express'; import { PrismaClient } from '@prisma/client'; +import multer from 'multer'; import path from 'path'; import fs from 'fs'; +import { config } from '../config/config'; const router = Router(); const prisma = new PrismaClient(); +// Utility function to get correct uploads directory path +const getUploadsDir = (subPath?: string): string => { + const baseDir = process.env.NODE_ENV === 'production' + ? path.join(process.cwd(), 'uploads') + : path.join(process.cwd(), '../../uploads'); + + return subPath ? path.join(baseDir, subPath) : baseDir; +}; + +// Configure multer for file uploads +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + const recipeNumber = req.body.recipeNumber || req.params.recipeNumber; + if (!recipeNumber) { + return cb(new Error('Recipe number is required'), ''); + } + + const uploadDir = getUploadsDir(recipeNumber); + + // Create directory if it doesn't exist + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); + } + + cb(null, uploadDir); + }, + filename: (req, file, cb) => { + const recipeNumber = req.body.recipeNumber || req.params.recipeNumber; + if (!recipeNumber) { + return cb(new Error('Recipe number is required'), ''); + } + + // Get existing files count to determine next index + const uploadDir = getUploadsDir(recipeNumber); + const existingFiles = fs.existsSync(uploadDir) + ? fs.readdirSync(uploadDir).filter(f => f.match(new RegExp(`^${recipeNumber}_\\d+\\.jpg$`))) + : []; + + const nextIndex = existingFiles.length; + const filename = `${recipeNumber}_${nextIndex}.jpg`; + + cb(null, filename); + } +}); + +const upload = multer({ + storage, + limits: { + fileSize: config.upload.maxFileSize, // 5MB + }, + fileFilter: (req, file, cb) => { + const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; + if (allowedTypes.includes(file.mimetype)) { + cb(null, true); + } else { + cb(new Error('Invalid file type. Only JPEG, PNG and WebP are allowed.')); + } + }, +}); + +// Upload images for a recipe +router.post('/upload/:recipeId', upload.array('images', 10), async (req: Request, res: Response, next: NextFunction) => { + try { + const { recipeId } = req.params; + const files = req.files as Express.Multer.File[]; + + if (!recipeId) { + return res.status(400).json({ + success: false, + message: 'Recipe ID is required', + }); + } + + if (!files || files.length === 0) { + return res.status(400).json({ + success: false, + message: 'No files uploaded', + }); + } + + // Get recipe to validate it exists and get recipe number + const recipe = await prisma.recipe.findUnique({ + where: { id: parseInt(recipeId) } + }); + + if (!recipe) { + return res.status(404).json({ + success: false, + message: 'Recipe not found', + }); + } + + // Create database entries for uploaded images + const imagePromises = files.map(file => { + const relativePath = `uploads/${recipe.recipeNumber}/${file.filename}`; + return prisma.recipeImage.create({ + data: { + recipeId: parseInt(recipeId), + filePath: relativePath, + } + }); + }); + + const images = await Promise.all(imagePromises); + + return res.status(201).json({ + success: true, + data: images, + message: `${files.length} images uploaded successfully`, + }); + } catch (error) { + // Clean up uploaded files if database operation fails + if (req.files) { + const files = req.files as Express.Multer.File[]; + files.forEach(file => { + if (fs.existsSync(file.path)) { + fs.unlinkSync(file.path); + } + }); + } + next(error); + } +}); + +// Delete an image +router.delete('/:id', async (req: Request, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + + if (!id) { + return res.status(400).json({ + success: false, + message: 'Image ID is required', + }); + } + + const image = await prisma.recipeImage.findUnique({ + where: { id: parseInt(id) } + }); + + if (!image) { + return res.status(404).json({ + success: false, + message: 'Image not found', + }); + } + + // Delete file from filesystem + const fullPath = path.join(process.cwd(), '../..', image.filePath); + if (fs.existsSync(fullPath)) { + fs.unlinkSync(fullPath); + } + + // Delete from database + await prisma.recipeImage.delete({ + where: { id: parseInt(id) } + }); + + return res.json({ + success: true, + message: 'Image deleted successfully', + }); + } catch (error) { + next(error); + } +}); + // Get all images for a recipe by recipe ID router.get('/recipe/:recipeId', async (req: Request, res: Response, next: NextFunction) => { try { @@ -46,7 +215,7 @@ router.get('/serve/:imagePath(*)', (req: Request, res: Response, next: NextFunct // Remove leading 'uploads/' if present to avoid duplication const cleanPath = imagePath.replace(/^uploads\//, ''); - const fullPath = path.join(process.cwd(), '../../uploads', cleanPath); + const fullPath = path.join(getUploadsDir(), cleanPath); console.log(`Serving image: ${imagePath} -> ${fullPath}`); @@ -60,9 +229,17 @@ router.get('/serve/:imagePath(*)', (req: Request, res: Response, next: NextFunct }); } - // Set CORS headers for images + // Set CORS headers for images - support multiple origins including local network + const allowedOrigins = ['http://localhost:5173', 'http://localhost:3000']; + const origin = req.headers.origin; + + // Check if CORS_ORIGIN is set to "*" for local network access + const corsOrigin = process.env.CORS_ORIGIN === '*' + ? (origin || '*') + : (origin && allowedOrigins.includes(origin)) ? origin : 'http://localhost:3000'; + res.set({ - 'Access-Control-Allow-Origin': 'http://localhost:5173', + 'Access-Control-Allow-Origin': corsOrigin, 'Access-Control-Allow-Credentials': 'true', 'Cache-Control': 'public, max-age=31536000', // Cache for 1 year }); diff --git a/nodejs-version/backend/src/routes/recipes.ts b/nodejs-version/backend/src/routes/recipes.ts index c341fb6..b455152 100644 --- a/nodejs-version/backend/src/routes/recipes.ts +++ b/nodejs-version/backend/src/routes/recipes.ts @@ -91,9 +91,17 @@ router.get('/:id', async (req: Request, res: Response, next: NextFunction) => { message: 'Recipe ID is required', }); } + + const recipeId = parseInt(id); + if (isNaN(recipeId)) { + return res.status(400).json({ + success: false, + message: 'Invalid recipe ID format', + }); + } const recipe = await prisma.recipe.findUnique({ - where: { id: parseInt(id) }, + where: { id: recipeId }, include: { images: true, ingredientsList: true, diff --git a/nodejs-version/frontend/Dockerfile b/nodejs-version/frontend/Dockerfile new file mode 100644 index 0000000..4db3db2 --- /dev/null +++ b/nodejs-version/frontend/Dockerfile @@ -0,0 +1,138 @@ +# Frontend Dockerfile +FROM node:18-alpine AS builder + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY . . + +# Build arguments for environment variables +# For local network: VITE_API_URL will be set dynamically by the container hostname +ARG VITE_API_URL +ENV VITE_API_URL=$VITE_API_URL + +# Build the application +RUN npm run build + +# Production stage with Nginx +FROM nginx:alpine AS production + +# Install curl for healthcheck +RUN apk add --no-cache curl + +# Copy custom nginx configuration +COPY < void; + accept?: string; + multiple?: boolean; + maxFiles?: number; + maxFileSize?: number; // in MB + disabled?: boolean; + className?: string; +} + +interface FileWithPreview extends File { + preview?: string; +} + +const FileUpload: React.FC = ({ + onFilesSelected, + accept = 'image/*', + multiple = true, + maxFiles = 10, + maxFileSize = 5, // 5MB default + disabled = false, + className = '', +}) => { + const [selectedFiles, setSelectedFiles] = useState([]); + const [dragOver, setDragOver] = useState(false); + const [errors, setErrors] = useState([]); + const fileInputRef = useRef(null); + + const validateFile = (file: File): string | null => { + // Check file size + if (file.size > maxFileSize * 1024 * 1024) { + return `Datei "${file.name}" ist zu groß. Maximum: ${maxFileSize}MB`; + } + + // Check file type + if (!file.type.startsWith('image/')) { + return `Datei "${file.name}" ist kein gültiges Bild`; + } + + return null; + }; + + const processFiles = (files: FileList | File[]) => { + const fileArray = Array.from(files); + const newErrors: string[] = []; + const validFiles: FileWithPreview[] = []; + + // Check total file count + if (selectedFiles.length + fileArray.length > maxFiles) { + newErrors.push(`Maximal ${maxFiles} Dateien erlaubt`); + setErrors(newErrors); + return; + } + + fileArray.forEach((file) => { + const error = validateFile(file); + if (error) { + newErrors.push(error); + } else { + // Create preview URL + const fileWithPreview = file as FileWithPreview; + fileWithPreview.preview = URL.createObjectURL(file); + validFiles.push(fileWithPreview); + } + }); + + if (newErrors.length > 0) { + setErrors(newErrors); + } else { + setErrors([]); + const updatedFiles = [...selectedFiles, ...validFiles]; + setSelectedFiles(updatedFiles); + onFilesSelected(updatedFiles); + } + }; + + const handleFileSelect = (e: ChangeEvent) => { + if (e.target.files && e.target.files.length > 0) { + processFiles(e.target.files); + } + }; + + const handleDrop = (e: DragEvent) => { + e.preventDefault(); + setDragOver(false); + + if (disabled) return; + + const files = e.dataTransfer.files; + if (files.length > 0) { + processFiles(files); + } + }; + + const handleDragOver = (e: DragEvent) => { + e.preventDefault(); + if (!disabled) { + setDragOver(true); + } + }; + + const handleDragLeave = (e: DragEvent) => { + e.preventDefault(); + setDragOver(false); + }; + + const removeFile = (index: number) => { + const fileToRemove = selectedFiles[index]; + if (fileToRemove.preview) { + URL.revokeObjectURL(fileToRemove.preview); + } + + const updatedFiles = selectedFiles.filter((_, i) => i !== index); + setSelectedFiles(updatedFiles); + onFilesSelected(updatedFiles); + }; + + const clearAll = () => { + selectedFiles.forEach(file => { + if (file.preview) { + URL.revokeObjectURL(file.preview); + } + }); + setSelectedFiles([]); + setErrors([]); + onFilesSelected([]); + }; + + return ( +
+ {/* Drop Zone */} +
!disabled && fileInputRef.current?.click()} + > +
+
📁
+

+ {selectedFiles.length > 0 + ? `${selectedFiles.length} Datei${selectedFiles.length > 1 ? 'en' : ''} ausgewählt` + : 'Bilder hier ablegen oder klicken zum Auswählen' + } +

+

+ Maximal {maxFiles} Dateien, je max. {maxFileSize}MB +

+
+ + +
+ + {/* Error Messages */} + {errors.length > 0 && ( +
+ {errors.map((error, index) => ( +
+ ⚠️ {error} +
+ ))} +
+ )} + + {/* File Previews */} + {selectedFiles.length > 0 && ( +
+
+

Ausgewählte Bilder ({selectedFiles.length})

+ +
+ +
+ {selectedFiles.map((file, index) => ( +
+
+ {file.name} { + // Clean up object URL after image loads + if (file.preview) { + URL.revokeObjectURL(file.preview); + } + }} + /> + +
+
+
+ {file.name.length > 20 ? `${file.name.substring(0, 17)}...` : file.name} +
+
+ {(file.size / 1024 / 1024).toFixed(2)} MB +
+
+
+ ))} +
+
+ )} +
+ ); +}; + +export default FileUpload; \ No newline at end of file diff --git a/nodejs-version/frontend/src/components/RecipeCreate.tsx b/nodejs-version/frontend/src/components/RecipeCreate.tsx index 6082131..9d27598 100644 --- a/nodejs-version/frontend/src/components/RecipeCreate.tsx +++ b/nodejs-version/frontend/src/components/RecipeCreate.tsx @@ -1,13 +1,16 @@ import React, { useState } from 'react'; import { useNavigate, Link } from 'react-router-dom'; -import { recipeApi } from '../services/api'; +import { recipeApi, imageApi } from '../services/api'; +import FileUpload from './FileUpload'; import './RecipeEdit.css'; // Reuse the same styles const RecipeCreate: React.FC = () => { const navigate = useNavigate(); const [loading, setLoading] = useState(false); + const [uploadProgress, setUploadProgress] = useState(0); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); + const [selectedFiles, setSelectedFiles] = useState([]); // Form state const [formData, setFormData] = useState({ @@ -41,13 +44,26 @@ const RecipeCreate: React.FC = () => { throw new Error('Titel ist erforderlich'); } + // First create the recipe const response = await recipeApi.createRecipe(formData); if (response.success) { + const recipeId = response.data.id; + + // Upload images if any were selected + if (selectedFiles.length > 0) { + try { + await imageApi.uploadImages(recipeId, selectedFiles, setUploadProgress); + } catch (uploadError) { + console.warn('Image upload failed:', uploadError); + // Don't fail the entire process if image upload fails + } + } + setSuccess(true); // Redirect to the new recipe detail page after a short delay setTimeout(() => { - navigate(`/recipes/${response.data.id}`); + navigate(`/recipes/${recipeId}`); }, 1500); } else { setError('Fehler beim Erstellen des Rezepts'); @@ -60,6 +76,10 @@ const RecipeCreate: React.FC = () => { } }; + const handleFilesSelected = (files: File[]) => { + setSelectedFiles(files); + }; + if (success) { return (
@@ -233,6 +253,27 @@ const RecipeCreate: React.FC = () => { />
+ {/* Image Upload Section */} +
+ +

+ Laden Sie Bilder für Ihr Rezept hoch. Das erste Bild wird als Hauptbild verwendet, + weitere Bilder werden den Zubereitungsschritten zugeordnet. +

+ + {uploadProgress > 0 && uploadProgress < 100 && ( +
+
+ {uploadProgress}% hochgeladen +
+ )} +
+ {/* Form Actions */}
✏️ Bearbeiten @@ -174,6 +221,64 @@ const RecipeDetail: React.FC = () => {
+ {/* Image Management Section */} + {editingImages && ( +
+

Bilder verwalten

+ + {/* Upload new images */} +
+

Neue Bilder hochladen

+ 0} + /> + {uploadProgress > 0 && uploadProgress < 100 && ( +
+
+ {uploadProgress}% hochgeladen +
+ )} +
+ + {/* Existing images */} + {recipe.images && recipe.images.length > 0 && ( +
+

Vorhandene Bilder ({recipe.images.length})

+
+ {recipe.images.map((image, index) => ( +
+
+ {`Bild + +
+
+ + {image.filePath.split('/').pop()} + + {image.filePath.includes('_0.jpg') && ( + Hauptbild + )} +
+
+ ))} +
+
+ )} +
+ )} + {/* Two Column Layout for Description/Ingredients and Preparation */}
{/* Left Column - Description and Ingredients */} diff --git a/nodejs-version/frontend/src/components/RecipeEdit.css b/nodejs-version/frontend/src/components/RecipeEdit.css index 7febdf6..98a3ddf 100644 --- a/nodejs-version/frontend/src/components/RecipeEdit.css +++ b/nodejs-version/frontend/src/components/RecipeEdit.css @@ -371,6 +371,37 @@ } } +/* Upload Progress in Forms */ +.form-group .upload-progress { + margin-top: 10px; + background: #e9ecef; + border-radius: 10px; + overflow: hidden; + height: 6px; + position: relative; +} + +.form-group .upload-progress-bar { + height: 100%; + background: linear-gradient(90deg, #007bff, #0056b3); + transition: width 0.3s ease; + border-radius: 10px; +} + +.upload-progress-text { + font-size: 0.9em; + color: #666; + margin-top: 5px; + display: block; +} + +.form-hint { + color: #666; + font-size: 0.9em; + margin-bottom: 10px; + line-height: 1.4; +} + /* High Contrast Mode */ @media (prefers-contrast: high) { .form-group input, diff --git a/nodejs-version/frontend/src/services/api.ts b/nodejs-version/frontend/src/services/api.ts index fb74ca1..cea6fb4 100644 --- a/nodejs-version/frontend/src/services/api.ts +++ b/nodejs-version/frontend/src/services/api.ts @@ -1,6 +1,21 @@ import axios from 'axios'; -const API_BASE_URL = 'http://localhost:3001/api'; +// Runtime API URL detection - works in the browser +const getApiBaseUrl = (): string => { + const hostname = window.location.hostname; + + if (hostname === 'localhost' || hostname === '127.0.0.1') { + // Local development + return 'http://localhost:3001/api'; + } else { + // Network access - use same host as frontend + return `http://${hostname}:3001/api`; + } +}; + +const API_BASE_URL = getApiBaseUrl(); + +console.log('🔗 API Base URL:', API_BASE_URL); // Debug log const api = axios.create({ baseURL: API_BASE_URL, @@ -135,6 +150,33 @@ export const imageApi = { getImageUrl: (imagePath: string): string => { return `${API_BASE_URL}/images/serve/${imagePath}`; }, + + // Upload images for a recipe + uploadImages: async (recipeId: number, files: File[], onProgress?: (progress: number) => void): Promise> => { + const formData = new FormData(); + files.forEach((file) => { + formData.append('images', file); + }); + + const response = await api.post(`/images/upload/${recipeId}`, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + onUploadProgress: (progressEvent) => { + if (onProgress && progressEvent.total) { + const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total); + onProgress(progress); + } + }, + }); + return response.data; + }, + + // Delete an image + deleteImage: async (imageId: number): Promise> => { + const response = await api.delete(`/images/${imageId}`); + return response.data; + }, }; // Health check diff --git a/setup-citysensor.sh b/setup-citysensor.sh new file mode 100755 index 0000000..e64e629 --- /dev/null +++ b/setup-citysensor.sh @@ -0,0 +1,68 @@ +#!/bin/bash +set -e + +echo "🏢 CitySensor Docker Registry Setup" +echo "=====================================" +echo "" + +# Check if required files exist +if [ ! -f .env.build.example ]; then + echo "❌ Error: .env.build.example not found!" + exit 1 +fi + +echo "📋 Setup Steps:" +echo "" + +echo "1️⃣ Create build configuration:" +echo " cp .env.build.example .env.registry" +echo " # Edit .env.registry with your CitySensor credentials" +echo "" + +echo "2️⃣ Required values in .env.registry:" +echo " DOCKER_REGISTRY=docker.citysensor.de" +echo " DOCKER_USERNAME=your_citysensor_username" +echo " DOCKER_PASSWORD=your_citysensor_password" +echo " DOMAIN=your.domain.com" +echo " API_BASE_URL=https://rezepte.your.domain.com/api" +echo "" + +echo "3️⃣ Build and push to CitySensor registry:" +echo " ./build-and-push.sh" +echo "" + +echo "4️⃣ For server deployment, create .env.production:" +echo " cp .env.traefik.example .env.production" +echo " # Edit with your server configuration" +echo "" + +echo "5️⃣ Server deployment files needed:" +echo " - docker-compose.traefik.yml" +echo " - .env.production" +echo " - *.sql files" +echo " - deploy-traefik.sh" +echo "" + +echo "6️⃣ Deploy on server:" +echo " ./deploy-traefik.sh" +echo "" + +echo "🔧 Example .env.registry content:" +echo "DOMAIN=example.com" +echo "ACME_EMAIL=admin@example.com" +echo "API_BASE_URL=https://rezepte.example.com/api" +echo "MYSQL_PASSWORD=secure_db_password" +echo "MYSQL_ROOT_PASSWORD=super_secure_root_password" +echo "DOCKER_REGISTRY=docker.citysensor.de" +echo "DOCKER_USERNAME=your_username" +echo "DOCKER_PASSWORD=your_password" +echo "BACKEND_IMAGE=docker.citysensor.de/rezepte-klaus-backend:latest" +echo "FRONTEND_IMAGE=docker.citysensor.de/rezepte-klaus-frontend:latest" +echo "" + +echo "✅ After setup, your app will be available at:" +echo " https://rezepte.your.domain.com" +echo "" + +echo "🛡️ Security Note:" +echo " Never commit .env.registry or .env.production to version control!" \ No newline at end of file diff --git a/setup-dev.sh b/setup-dev.sh new file mode 100755 index 0000000..f7ab9c2 --- /dev/null +++ b/setup-dev.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +# Farben für Output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${GREEN}Rezepte Klaus - Development Setup${NC}" +echo "================================================" + +# Node.js Version prüfen +if ! command -v node &> /dev/null; then + echo -e "${RED}❌ Node.js ist nicht installiert${NC}" + echo "Bitte installiere Node.js: https://nodejs.org/" + exit 1 +fi + +NODE_VERSION=$(node --version) +echo -e "${GREEN}✅ Node.js Version: ${NODE_VERSION}${NC}" + +# Überprüfe ob MySQL läuft (lokal) +if command -v mysql &> /dev/null; then + if mysql -u root -e "SELECT 1;" &> /dev/null; then + echo -e "${GREEN}✅ MySQL ist verfügbar${NC}" + else + echo -e "${YELLOW}⚠️ MySQL läuft nicht oder benötigt Passwort${NC}" + fi +else + echo -e "${YELLOW}⚠️ MySQL nicht gefunden - Docker MySQL wird empfohlen${NC}" +fi + +# Environment-Datei für Development +if [ ! -f .env ]; then + echo -e "${YELLOW}📝 Kopiere .env.development zu .env${NC}" + cp .env.development .env +else + echo -e "${GREEN}✅ .env Datei bereits vorhanden${NC}" +fi + +# Backend Setup +echo -e "${BLUE}🔧 Backend Setup...${NC}" +cd nodejs-version/backend + +if [ ! -d node_modules ]; then + echo -e "${YELLOW}📦 Installiere Backend Dependencies...${NC}" + npm install +else + echo -e "${GREEN}✅ Backend Dependencies bereits installiert${NC}" +fi + +# Prisma Setup +echo -e "${YELLOW}🗃️ Generiere Prisma Client...${NC}" +npx prisma generate + +# Frontend Setup +echo -e "${BLUE}🎨 Frontend Setup...${NC}" +cd ../../frontend + +if [ ! -d node_modules ]; then + echo -e "${YELLOW}📦 Installiere Frontend Dependencies...${NC}" + npm install +else + echo -e "${GREEN}✅ Frontend Dependencies bereits installiert${NC}" +fi + +# Upload Ordner erstellen +cd .. +echo -e "${YELLOW}📁 Erstelle Upload-Ordner...${NC}" +mkdir -p uploads + +# Legacy Uploads kopieren falls vorhanden +if [ -d "upload" ]; then + echo -e "${YELLOW}📋 Kopiere bestehende Uploads...${NC}" + cp -r upload/* uploads/ 2>/dev/null || true +fi + +echo "" +echo -e "${GREEN}🎉 Development Setup abgeschlossen!${NC}" +echo "" +echo -e "${BLUE}Development Server starten:${NC}" +echo "" +echo -e "${YELLOW}Backend (Terminal 1):${NC}" +echo " cd nodejs-version/backend" +echo " npm run dev" +echo "" +echo -e "${YELLOW}Frontend (Terminal 2):${NC}" +echo " cd frontend" +echo " npm run dev" +echo "" +echo -e "${GREEN}Dann öffne: http://localhost:5173${NC}" +echo "" \ No newline at end of file diff --git a/start-local-network.sh b/start-local-network.sh new file mode 100755 index 0000000..8b4c714 --- /dev/null +++ b/start-local-network.sh @@ -0,0 +1,103 @@ +#!/bin/bash +set -e + +echo "🌐 Starting Rezepte Klaus for Local Network Access" +echo "==================================================" + +# Function to detect host IP +detect_host_ip() { + # Try different methods to get the local IP + local ip="" + + # Method 1: macOS WiFi + if command -v ipconfig &> /dev/null; then + ip=$(ipconfig getifaddr en0 2>/dev/null || echo "") + if [ -n "$ip" ]; then + echo "$ip" + return + fi + # Try Ethernet on macOS + ip=$(ipconfig getifaddr en1 2>/dev/null || echo "") + if [ -n "$ip" ]; then + echo "$ip" + return + fi + fi + + # Method 2: Linux/Unix + if command -v ip &> /dev/null; then + ip=$(ip route get 1 2>/dev/null | awk '{print $7; exit}' || echo "") + if [ -n "$ip" ]; then + echo "$ip" + return + fi + fi + + # Method 3: Alternative Linux method + if command -v hostname &> /dev/null; then + ip=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "") + if [ -n "$ip" ]; then + echo "$ip" + return + fi + fi + + # Fallback + echo "192.168.1.100" +} + +# Detect the host IP +HOST_IP=$(detect_host_ip) + +echo "🔍 Detected Host IP: $HOST_IP" +echo "" + +# Verify IP looks valid +if [[ ! $HOST_IP =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then + echo "⚠️ Warning: Detected IP '$HOST_IP' doesn't look valid." + echo "Please check your network connection or manually set HOST_IP in .env.local-network" + echo "" + read -p "Do you want to continue anyway? (y/N): " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi +fi + +# Export HOST_IP for docker-compose +export HOST_IP + +echo "🛑 Stopping any existing containers..." +docker-compose -f docker-compose.local-network.yml down + +echo "🏗️ Building and starting containers for local network access..." +docker-compose -f docker-compose.local-network.yml up --build -d + +echo "⏳ Waiting for services to start..." +sleep 20 + +echo "🔍 Checking service health..." +if docker-compose -f docker-compose.local-network.yml ps | grep -q "Up"; then + echo "✅ Services started successfully!" + echo "" + echo "🌐 Your application is now accessible from:" + echo " Local machine: http://localhost:3000" + echo " Network access: http://$HOST_IP:3000" + echo " API endpoint: http://$HOST_IP:3001/api" + echo "" + echo "📊 Service Status:" + docker-compose -f docker-compose.local-network.yml ps + echo "" + echo "💡 Share this URL with other devices on your network:" + echo " http://$HOST_IP:3000" +else + echo "❌ Failed to start services. Check logs:" + docker-compose -f docker-compose.local-network.yml logs --tail=20 + exit 1 +fi + +echo "" +echo "📋 Useful commands:" +echo " View logs: docker-compose -f docker-compose.local-network.yml logs -f" +echo " Stop: docker-compose -f docker-compose.local-network.yml down" +echo " Restart: docker-compose -f docker-compose.local-network.yml restart" \ No newline at end of file diff --git a/uploads/.DS_Store b/uploads/.DS_Store index b8dd6dac8c488b00d7ed00937e005c686a193e88..d7f9ad553efab724e65dd329ced194a2bc801a7c 100644 GIT binary patch delta 82 zcmZoMXfc?O%(!(jBh&K91|lqz`58rkJQc=5718QyBLf`;gIXPhYD*By(!`>+mXkwN jS>HM+K07BjFTZ{AK1O-Qj?MQNHQ6RMux@7O_{$Ff&w?02 delta 84 zcmZoMXfc?O%(!dvN5Bbhhp4i?bx?eEPHtX)&*Ytq@{GNkZ!&7KZD!~A%MSoD*BL1Q