added docker setup

This commit is contained in:
Jan Felix Wiebe 2025-07-11 14:33:20 +02:00
parent 57ae9c3320
commit be5ce63add
12 changed files with 509 additions and 20 deletions

185
README-Docker.md Normal file
View file

@ -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.

30
backend/.dockerignore Normal file
View file

@ -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/

32
backend/Dockerfile Normal file
View file

@ -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"]

View file

@ -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)

43
docker-compose.yml Normal file
View file

@ -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

12
frontend/.dockerignore Normal file
View file

@ -0,0 +1,12 @@
node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.nyc_output
coverage
.DS_Store
.vscode
*.log
dist

31
frontend/Dockerfile Normal file
View file

@ -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;"]

31
frontend/nginx.conf Normal file
View file

@ -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;
}

View file

@ -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',
})

View file

@ -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()
}

70
nginx/conf.d/default.conf Normal file
View file

@ -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;
}
}

54
nginx/nginx.conf Normal file
View file

@ -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;
}