diff --git a/README-Docker.md b/README-Docker.md new file mode 100644 index 0000000..e3b83b4 --- /dev/null +++ b/README-Docker.md @@ -0,0 +1,185 @@ +# Tschunk Order - Docker Deployment + +Diese Anleitung beschreibt, wie Sie die Tschunk Order Anwendung mit Docker Compose deployen können. + +## Voraussetzungen + +- Docker +- Docker Compose + +## Projektstruktur + +``` +tschunkorder/ +├── docker-compose.yml # Haupt-Konfiguration +├── frontend/ +│ ├── Dockerfile # Frontend Container +│ ├── nginx.conf # Frontend Nginx-Konfiguration +│ └── .dockerignore +├── backend/ +│ ├── Dockerfile # Backend Container +│ └── .dockerignore +└── nginx/ + ├── nginx.conf # Haupt-Nginx-Konfiguration + └── conf.d/ + └── default.conf # Reverse Proxy-Konfiguration +``` + +## Services + +### 1. Frontend (Vue.js + Vite) +- **Port**: Intern 80, extern über Nginx +- **Technologie**: Vue.js 3 mit Vite +- **Container**: `tschunk-frontend` + +### 2. Backend (FastAPI) +- **Port**: Intern 8000, extern 8000 (optional) +- **Technologie**: FastAPI mit Uvicorn +- **Container**: `tschunk-backend` + +### 3. Nginx Reverse Proxy +- **Port**: 80 (HTTP), 443 (HTTPS, optional) +- **Funktion**: Reverse Proxy für Frontend und Backend +- **Container**: `tschunk-nginx` + +## Deployment + +### 1. Anwendung starten + +```bash +# Alle Services bauen und starten +docker-compose up --build + +# Im Hintergrund starten +docker-compose up --build -d +``` + +### 2. Anwendung stoppen + +```bash +# Services stoppen +docker-compose down + +# Services und Volumes löschen +docker-compose down -v +``` + +### 3. Logs anzeigen + +```bash +# Alle Logs +docker-compose logs + +# Spezifischer Service +docker-compose logs frontend +docker-compose logs backend +docker-compose logs nginx + +# Logs folgen +docker-compose logs -f +``` + +## Zugriff auf die Anwendung + +- **Frontend**: http://localhost/ (oder IP-Adresse) +- **Backend API**: http://localhost/api/ (oder IP-Adresse) +- **WebSocket**: ws://localhost/api/ws (oder IP-Adresse) +- **Health Check**: http://localhost/health (oder IP-Adresse) + +## API-Endpunkte + +- `GET /api/orders` - Alle Bestellungen abrufen +- `POST /api/orders` - Neue Bestellung erstellen +- `DELETE /api/orders/{id}` - Bestellung löschen +- `GET /api/drinks` - Verfügbare Getränke + +## WebSocket + +Der WebSocket-Endpunkt ist unter `ws://localhost/api/ws` verfügbar und unterstützt: +- Echtzeit-Updates bei neuen/gelöschten Bestellungen +- Ping/Pong für Verbindungsüberwachung +- Bestellungen erstellen/löschen über WebSocket + +## Konfiguration + +### Umgebungsvariablen + +#### Frontend +- `VITE_API_BASE_URL`: Backend API URL (Standard: http://backend:8000) + +#### Backend +- `PYTHONUNBUFFERED`: Python Output nicht puffern (Standard: 1) + +### Nginx-Konfiguration + +Die Nginx-Konfiguration befindet sich in: +- `nginx/nginx.conf` - Haupt-Konfiguration +- `nginx/conf.d/default.conf` - Server-Konfiguration + +#### Features +- Rate Limiting für API und WebSocket +- Gzip-Kompression +- Security Headers +- WebSocket-Support +- SPA-Routing für Frontend +- Vereinfachte Proxy-Logik (alle API-Endpunkte unter /api) + +## Troubleshooting + +### Container startet nicht +```bash +# Logs prüfen +docker-compose logs [service-name] + +# Container neu bauen +docker-compose build --no-cache [service-name] +``` + +### Netzwerk-Probleme +```bash +# Netzwerk-Status prüfen +docker network ls +docker network inspect tschunkorder_tschunk-network +``` + +### Port-Konflikte +Falls Port 80 bereits belegt ist, ändern Sie in `docker-compose.yml`: +```yaml +nginx: + ports: + - "8080:80" # Statt "80:80" +``` + +## Entwicklung + +### Hot Reload (Entwicklung) +Für die Entwicklung können Sie die Services einzeln starten: + +```bash +# Backend mit Hot Reload +cd backend +uvicorn main:app --reload --host 0.0.0.0 --port 8000 + +# Frontend mit Hot Reload +cd frontend +npm run dev +``` + +### Datenbank-Persistenz +Die aktuelle Implementierung verwendet eine In-Memory-Datenbank. Für Produktionsumgebungen sollten Sie eine persistente Datenbank hinzufügen. + +## Sicherheit + +- Alle Services laufen in isolierten Containern +- Nginx bietet Rate Limiting und Security Headers +- Backend läuft als non-root User +- WebSocket-Verbindungen sind begrenzt + +## Monitoring + +### Health Checks +- Backend: Automatischer Health Check alle 30 Sekunden +- Nginx: `/health` Endpunkt + +### Logs +Alle Services loggen in stdout/stderr und können über `docker-compose logs` abgerufen werden. \ No newline at end of file diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..8561326 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,30 @@ +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +env +pip-log.txt +pip-delete-this-directory.txt +.tox +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.log +.git +.mypy_cache +.pytest_cache +.hypothesis +.DS_Store +.env +.venv +venv/ +ENV/ +env/ +.venv/ +*.egg-info/ +dist/ +build/ \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..3dc1f05 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,32 @@ +FROM python:3.11-slim + +# Set working directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first for better caching +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Create non-root user +RUN useradd --create-home --shell /bin/bash app && chown -R app:app /app +USER app + +# Expose port +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD python -c "import requests; requests.get('http://localhost:8000/orders')" || exit 1 + +# Start the application +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 62eae59..08d01a2 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,4 +1,4 @@ -from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect +from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, APIRouter from typing import List from models import CreateOrderRequest, Order, DrinkType, MateType from database import db @@ -11,11 +11,14 @@ app = FastAPI( version="1.0.0" ) +# API Router mit /api Prefix +api_router = APIRouter(prefix="/api") + # Datenbank-Referenz im WebSocketManager setzen websocket_manager.set_database(db) -@app.websocket("/ws") +@app.websocket("/api/ws") async def websocket_endpoint(websocket: WebSocket): """ WebSocket-Endpunkt für Echtzeit-Updates der Bestellungen. @@ -74,7 +77,7 @@ async def websocket_endpoint(websocket: WebSocket): websocket_manager.disconnect(websocket) -@app.post("/orders", response_model=Order, status_code=201) +@api_router.post("/orders", response_model=Order, status_code=201) async def create_order(order_request: CreateOrderRequest): """ Erstellt eine neue Bestellung. @@ -90,7 +93,7 @@ async def create_order(order_request: CreateOrderRequest): return order -@app.get("/orders", response_model=List[Order]) +@api_router.get("/orders", response_model=List[Order]) async def get_all_orders(): """ Gibt alle Bestellungen zurück. @@ -99,7 +102,7 @@ async def get_all_orders(): return orders -@app.delete("/orders/{order_id}") +@api_router.delete("/orders/{order_id}") async def delete_order(order_id: str): """ Löscht eine spezifische Bestellung. @@ -115,7 +118,7 @@ async def delete_order(order_id: str): return {"message": f"Bestellung {order_id} wurde erfolgreich gelöscht"} -@app.get("/drinks") +@api_router.get("/drinks") async def get_available_drinks(): """ Gibt alle verfügbaren Getränketypen zurück. @@ -126,6 +129,10 @@ async def get_available_drinks(): } +# API Router zur App hinzufügen +app.include_router(api_router) + + if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..70b04da --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +services: + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: tschunk-frontend + restart: unless-stopped + networks: + - tschunk-network + environment: + - VITE_API_BASE_URL=http://backend:8000 + depends_on: + - backend + + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: tschunk-backend + restart: unless-stopped + networks: + - tschunk-network + environment: + - PYTHONUNBUFFERED=1 + + nginx: + image: nginx:alpine + container_name: tschunk-nginx + restart: unless-stopped + ports: + - "80:80" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/conf.d:/etc/nginx/conf.d:ro + networks: + - tschunk-network + depends_on: + - frontend + - backend + +networks: + tschunk-network: + driver: bridge \ No newline at end of file diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..d03c0c0 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,12 @@ +node_modules +npm-debug.log +.git +.gitignore +README.md +.env +.nyc_output +coverage +.DS_Store +.vscode +*.log +dist \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..df314c1 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,31 @@ +# Build stage +FROM node:20-alpine as build + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install all dependencies (including devDependencies for build) +RUN npm ci + +# Copy source code +COPY . . + +# Build the application +RUN npm run build + +# Production stage +FROM nginx:alpine + +# Copy built files from build stage +COPY --from=build /app/dist /usr/share/nginx/html + +# Copy custom nginx configuration for SPA +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Expose port +EXPOSE 80 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..69ed9b2 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,31 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied expired no-cache no-store private auth; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript; + + # Handle SPA routing + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; +} \ No newline at end of file diff --git a/frontend/src/stores/orderStore.ts b/frontend/src/stores/orderStore.ts index a9b8db1..43aadfe 100644 --- a/frontend/src/stores/orderStore.ts +++ b/frontend/src/stores/orderStore.ts @@ -25,8 +25,10 @@ export const useOrderStore = defineStore('orders', () => { const connectWebSocket = () => { try { connectionStatus.value = 'connecting' - // WebSocket-Verbindung zum Backend - wsConnection.value = new WebSocket('ws://localhost:8000/ws') + // WebSocket-Verbindung zum Backend über relative URL + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + const wsUrl = `${protocol}//${window.location.host}/api/ws` + wsConnection.value = new WebSocket(wsUrl) wsConnection.value.onopen = () => { console.log('WebSocket verbunden') @@ -168,7 +170,7 @@ export const useOrderStore = defineStore('orders', () => { const createOrderHttp = async (orderRequest: CreateOrderRequest) => { try { - const response = await fetch('http://localhost:8000/orders', { + const response = await fetch('/api/orders', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -233,7 +235,7 @@ export const useOrderStore = defineStore('orders', () => { const deleteOrderHttp = async (orderId: string) => { try { - const response = await fetch(`http://localhost:8000/orders/${orderId}`, { + const response = await fetch(`/api/orders/${orderId}`, { method: 'DELETE', }) diff --git a/frontend/src/views/CreateOrderView.vue b/frontend/src/views/CreateOrderView.vue index 97b66d3..db8e2b6 100644 --- a/frontend/src/views/CreateOrderView.vue +++ b/frontend/src/views/CreateOrderView.vue @@ -79,6 +79,7 @@ import { useRouter } from 'vue-router' import { useOrderStore } from '@/stores/orderStore' import { DrinkType, MateType, type Drink } from '@/types/order' import DrinkCard from '@/components/NewDrinkCard.vue' +import { type Order } from '@/types/order' const router = useRouter() const orderStore = useOrderStore() @@ -149,7 +150,7 @@ const submitOrder = async () => { try { const newOrder = await orderStore.createOrder({ drinks: validDrinks.value - }) + }) as Order toast.value = { show: true, message: `Bestellung erfolgreich! ID: ${newOrder.id}` @@ -167,15 +168,6 @@ const submitOrder = async () => { // Modal- und createNewOrder-Logik entfernt -const closeSuccessModal = () => { - showSuccessModal.value = false -} - -const createNewOrder = () => { - showSuccessModal.value = false - resetForm() -} - const retryConnection = () => { orderStore.connectWebSocket() } diff --git a/nginx/conf.d/default.conf b/nginx/conf.d/default.conf new file mode 100644 index 0000000..139c9af --- /dev/null +++ b/nginx/conf.d/default.conf @@ -0,0 +1,70 @@ +# Upstream definitions +upstream frontend { + server frontend:80; +} + +upstream backend { + server backend:8000; +} + +# Main server block +server { + listen 80; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + + # API routes - proxy to backend + location /api/ { + limit_req zone=api burst=20 nodelay; + proxy_pass http://backend; + 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_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # WebSocket endpoint - proxy to backend + location /api/ws { + limit_req zone=ws burst=50 nodelay; + proxy_pass http://backend; + 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_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_connect_timeout 7d; + proxy_send_timeout 7d; + proxy_read_timeout 7d; + } + + # Frontend routes - proxy to frontend + location / { + proxy_pass http://frontend; + 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_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } +} \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..a7c1117 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,54 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; + use epoll; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Logging + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + # Basic settings + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + client_max_body_size 16M; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied any; + gzip_comp_level 6; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/json + application/javascript + application/xml+rss + application/atom+xml + image/svg+xml; + + # Rate limiting + limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; + limit_req_zone $binary_remote_addr zone=ws:10m rate=30r/s; + + # Include server configurations + include /etc/nginx/conf.d/*.conf; +} \ No newline at end of file