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 typing import List
from models import CreateOrderRequest, Order, DrinkType, MateType from models import CreateOrderRequest, Order, DrinkType, MateType
from database import db from database import db
@ -11,11 +11,14 @@ app = FastAPI(
version="1.0.0" version="1.0.0"
) )
# API Router mit /api Prefix
api_router = APIRouter(prefix="/api")
# Datenbank-Referenz im WebSocketManager setzen # Datenbank-Referenz im WebSocketManager setzen
websocket_manager.set_database(db) websocket_manager.set_database(db)
@app.websocket("/ws") @app.websocket("/api/ws")
async def websocket_endpoint(websocket: WebSocket): async def websocket_endpoint(websocket: WebSocket):
""" """
WebSocket-Endpunkt für Echtzeit-Updates der Bestellungen. WebSocket-Endpunkt für Echtzeit-Updates der Bestellungen.
@ -74,7 +77,7 @@ async def websocket_endpoint(websocket: WebSocket):
websocket_manager.disconnect(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): async def create_order(order_request: CreateOrderRequest):
""" """
Erstellt eine neue Bestellung. Erstellt eine neue Bestellung.
@ -90,7 +93,7 @@ async def create_order(order_request: CreateOrderRequest):
return order return order
@app.get("/orders", response_model=List[Order]) @api_router.get("/orders", response_model=List[Order])
async def get_all_orders(): async def get_all_orders():
""" """
Gibt alle Bestellungen zurück. Gibt alle Bestellungen zurück.
@ -99,7 +102,7 @@ async def get_all_orders():
return orders return orders
@app.delete("/orders/{order_id}") @api_router.delete("/orders/{order_id}")
async def delete_order(order_id: str): async def delete_order(order_id: str):
""" """
Löscht eine spezifische Bestellung. 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"} return {"message": f"Bestellung {order_id} wurde erfolgreich gelöscht"}
@app.get("/drinks") @api_router.get("/drinks")
async def get_available_drinks(): async def get_available_drinks():
""" """
Gibt alle verfügbaren Getränketypen zurück. 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__": if __name__ == "__main__":
import uvicorn import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000) 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 = () => { const connectWebSocket = () => {
try { try {
connectionStatus.value = 'connecting' connectionStatus.value = 'connecting'
// WebSocket-Verbindung zum Backend // WebSocket-Verbindung zum Backend über relative URL
wsConnection.value = new WebSocket('ws://localhost:8000/ws') const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const wsUrl = `${protocol}//${window.location.host}/api/ws`
wsConnection.value = new WebSocket(wsUrl)
wsConnection.value.onopen = () => { wsConnection.value.onopen = () => {
console.log('WebSocket verbunden') console.log('WebSocket verbunden')
@ -168,7 +170,7 @@ export const useOrderStore = defineStore('orders', () => {
const createOrderHttp = async (orderRequest: CreateOrderRequest) => { const createOrderHttp = async (orderRequest: CreateOrderRequest) => {
try { try {
const response = await fetch('http://localhost:8000/orders', { const response = await fetch('/api/orders', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -233,7 +235,7 @@ export const useOrderStore = defineStore('orders', () => {
const deleteOrderHttp = async (orderId: string) => { const deleteOrderHttp = async (orderId: string) => {
try { try {
const response = await fetch(`http://localhost:8000/orders/${orderId}`, { const response = await fetch(`/api/orders/${orderId}`, {
method: 'DELETE', method: 'DELETE',
}) })

View file

@ -79,6 +79,7 @@ import { useRouter } from 'vue-router'
import { useOrderStore } from '@/stores/orderStore' import { useOrderStore } from '@/stores/orderStore'
import { DrinkType, MateType, type Drink } from '@/types/order' import { DrinkType, MateType, type Drink } from '@/types/order'
import DrinkCard from '@/components/NewDrinkCard.vue' import DrinkCard from '@/components/NewDrinkCard.vue'
import { type Order } from '@/types/order'
const router = useRouter() const router = useRouter()
const orderStore = useOrderStore() const orderStore = useOrderStore()
@ -149,7 +150,7 @@ const submitOrder = async () => {
try { try {
const newOrder = await orderStore.createOrder({ const newOrder = await orderStore.createOrder({
drinks: validDrinks.value drinks: validDrinks.value
}) }) as Order
toast.value = { toast.value = {
show: true, show: true,
message: `Bestellung erfolgreich! ID: ${newOrder.id}` message: `Bestellung erfolgreich! ID: ${newOrder.id}`
@ -167,15 +168,6 @@ const submitOrder = async () => {
// Modal- und createNewOrder-Logik entfernt // Modal- und createNewOrder-Logik entfernt
const closeSuccessModal = () => {
showSuccessModal.value = false
}
const createNewOrder = () => {
showSuccessModal.value = false
resetForm()
}
const retryConnection = () => { const retryConnection = () => {
orderStore.connectWebSocket() 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;
}