feat: Update Docker and Kubernetes for database infrastructure

- Update backend Dockerfile with PostgreSQL deps and entrypoint
- Add entrypoint.sh with db/redis wait and auto-migration
- Add /ready endpoint for Kubernetes readiness probe
- Enhance /health endpoint with database and Redis status
- Update k8s deployment with PostgreSQL and Redis services
- Add proper secrets management for database credentials
- Update k8s readiness probe to use /ready endpoint
This commit is contained in:
Sepehr 2025-12-31 10:58:41 +01:00
parent 550f3516db
commit 3d37ce4582
4 changed files with 335 additions and 6 deletions

View File

@ -9,6 +9,7 @@ WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \ build-essential \
libmagic1 \ libmagic1 \
libpq-dev \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Copy requirements first for better caching # Copy requirements first for better caching
@ -28,6 +29,7 @@ WORKDIR /app
# Install runtime dependencies only # Install runtime dependencies only
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
libmagic1 \ libmagic1 \
libpq5 \
curl \ curl \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& apt-get clean && apt-get clean
@ -46,20 +48,24 @@ RUN mkdir -p /app/uploads /app/outputs /app/logs /app/temp \
# Copy application code # Copy application code
COPY --chown=translator:translator . . COPY --chown=translator:translator . .
# Make entrypoint executable
RUN chmod +x docker/backend/entrypoint.sh
# Switch to non-root user # Switch to non-root user
USER translator USER translator
# Environment variables # Environment variables
ENV PYTHONUNBUFFERED=1 \ ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \ PYTHONDONTWRITEBYTECODE=1 \
PORT=8000 PORT=8000 \
WORKERS=4
# Health check # Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
CMD curl -f http://localhost:${PORT}/health || exit 1 CMD curl -f http://localhost:${PORT}/health || exit 1
# Expose port # Expose port
EXPOSE ${PORT} EXPOSE ${PORT}
# Run with uvicorn # Use entrypoint script to handle migrations and startup
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"] ENTRYPOINT ["docker/backend/entrypoint.sh"]

View File

@ -0,0 +1,66 @@
#!/bin/bash
set -e
echo "🚀 Starting Document Translation API..."
# Wait for database to be ready (if DATABASE_URL is set)
if [ -n "$DATABASE_URL" ]; then
echo "⏳ Waiting for database to be ready..."
# Extract host and port from DATABASE_URL
# postgresql://user:pass@host:port/db
DB_HOST=$(echo $DATABASE_URL | sed -e 's/.*@\([^:]*\):.*/\1/')
DB_PORT=$(echo $DATABASE_URL | sed -e 's/.*:\([0-9]*\)\/.*/\1/')
# Wait up to 30 seconds for database
for i in {1..30}; do
if python -c "
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.connect(('$DB_HOST', $DB_PORT))
s.close()
exit(0)
except:
exit(1)
" 2>/dev/null; then
echo "✅ Database is ready!"
break
fi
echo " Waiting for database... ($i/30)"
sleep 1
done
# Run database migrations
echo "📦 Running database migrations..."
alembic upgrade head || echo "⚠️ Migration skipped (may already be up to date)"
fi
# Wait for Redis if configured
if [ -n "$REDIS_URL" ]; then
echo "⏳ Waiting for Redis..."
REDIS_HOST=$(echo $REDIS_URL | sed -e 's/redis:\/\/\([^:]*\):.*/\1/')
REDIS_PORT=$(echo $REDIS_URL | sed -e 's/.*:\([0-9]*\)\/.*/\1/')
for i in {1..10}; do
if python -c "
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.connect(('$REDIS_HOST', $REDIS_PORT))
s.close()
exit(0)
except:
exit(1)
" 2>/dev/null; then
echo "✅ Redis is ready!"
break
fi
echo " Waiting for Redis... ($i/10)"
sleep 1
done
fi
# Start the application
echo "🎯 Starting uvicorn..."
exec uvicorn main:app --host 0.0.0.0 --port ${PORT:-8000} --workers ${WORKERS:-4}

View File

@ -25,6 +25,7 @@ data:
RATE_LIMIT_REQUESTS_PER_MINUTE: "60" RATE_LIMIT_REQUESTS_PER_MINUTE: "60"
RATE_LIMIT_TRANSLATIONS_PER_MINUTE: "10" RATE_LIMIT_TRANSLATIONS_PER_MINUTE: "10"
LOG_LEVEL: "INFO" LOG_LEVEL: "INFO"
# Database and Redis URLs are in secrets for security
--- ---
# Secret for sensitive data # Secret for sensitive data
@ -35,10 +36,17 @@ metadata:
namespace: translate-api namespace: translate-api
type: Opaque type: Opaque
stringData: stringData:
# CHANGE ALL THESE IN PRODUCTION!
ADMIN_USERNAME: "admin" ADMIN_USERNAME: "admin"
ADMIN_PASSWORD: "changeme123" # Change in production! ADMIN_PASSWORD_HASH: "" # Use: echo -n 'yourpassword' | sha256sum
JWT_SECRET: "" # Generate with: openssl rand -hex 32
DATABASE_URL: "postgresql://translate:translate_secret@postgres-service:5432/translate_db"
REDIS_URL: "redis://redis-service:6379/0"
DEEPL_API_KEY: "" DEEPL_API_KEY: ""
OPENAI_API_KEY: "" OPENAI_API_KEY: ""
OPENROUTER_API_KEY: ""
STRIPE_SECRET_KEY: ""
STRIPE_WEBHOOK_SECRET: ""
--- ---
# Backend Deployment # Backend Deployment
@ -79,16 +87,18 @@ spec:
cpu: "1000m" cpu: "1000m"
readinessProbe: readinessProbe:
httpGet: httpGet:
path: /health path: /ready
port: 8000 port: 8000
initialDelaySeconds: 10 initialDelaySeconds: 10
periodSeconds: 10 periodSeconds: 10
failureThreshold: 3
livenessProbe: livenessProbe:
httpGet: httpGet:
path: /health path: /health
port: 8000 port: 8000
initialDelaySeconds: 30 initialDelaySeconds: 30
periodSeconds: 30 periodSeconds: 30
failureThreshold: 3
volumeMounts: volumeMounts:
- name: uploads - name: uploads
mountPath: /app/uploads mountPath: /app/uploads
@ -286,3 +296,189 @@ spec:
target: target:
type: Utilization type: Utilization
averageUtilization: 80 averageUtilization: 80
---
# ============================================
# PostgreSQL Database
# ============================================
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
namespace: translate-api
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
namespace: translate-api
labels:
app: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
ports:
- containerPort: 5432
env:
- name: POSTGRES_USER
value: "translate"
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secrets
key: password
- name: POSTGRES_DB
value: "translate_db"
- name: PGDATA
value: "/var/lib/postgresql/data/pgdata"
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
readinessProbe:
exec:
command:
- pg_isready
- -U
- translate
- -d
- translate_db
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
exec:
command:
- pg_isready
- -U
- translate
- -d
- translate_db
initialDelaySeconds: 30
periodSeconds: 30
volumes:
- name: postgres-data
persistentVolumeClaim:
claimName: postgres-pvc
---
apiVersion: v1
kind: Secret
metadata:
name: postgres-secrets
namespace: translate-api
type: Opaque
stringData:
password: "translate_secret_change_me" # CHANGE IN PRODUCTION!
---
apiVersion: v1
kind: Service
metadata:
name: postgres-service
namespace: translate-api
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
type: ClusterIP
---
# ============================================
# Redis Cache
# ============================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: translate-api
labels:
app: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7-alpine
command:
- redis-server
- --appendonly
- "yes"
- --maxmemory
- "256mb"
- --maxmemory-policy
- "allkeys-lru"
ports:
- containerPort: 6379
resources:
requests:
memory: "128Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "200m"
readinessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 30
periodSeconds: 30
volumeMounts:
- name: redis-data
mountPath: /data
volumes:
- name: redis-data
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: redis-service
namespace: translate-api
spec:
selector:
app: redis
ports:
- port: 6379
targetPort: 6379
type: ClusterIP

61
main.py
View File

@ -305,11 +305,36 @@ async def health_check():
health_status = await health_checker.check_health() health_status = await health_checker.check_health()
status_code = 200 if health_status.get("status") == "healthy" else 503 status_code = 200 if health_status.get("status") == "healthy" else 503
# Check database connection
db_status = {"status": "not_configured"}
try:
from database.connection import check_db_connection
if check_db_connection():
db_status = {"status": "healthy"}
else:
db_status = {"status": "unhealthy"}
except Exception as e:
db_status = {"status": "error", "error": str(e)}
# Check Redis connection
redis_status = {"status": "not_configured"}
redis_client = get_redis_client()
if redis_client:
try:
redis_client.ping()
redis_status = {"status": "healthy"}
except Exception as e:
redis_status = {"status": "unhealthy", "error": str(e)}
elif redis_client is False:
redis_status = {"status": "connection_failed"}
return JSONResponse( return JSONResponse(
status_code=status_code, status_code=status_code,
content={ content={
"status": health_status.get("status", "unknown"), "status": health_status.get("status", "unknown"),
"translation_service": config.TRANSLATION_SERVICE, "translation_service": config.TRANSLATION_SERVICE,
"database": db_status,
"redis": redis_status,
"memory": health_status.get("memory", {}), "memory": health_status.get("memory", {}),
"disk": health_status.get("disk", {}), "disk": health_status.get("disk", {}),
"cleanup_service": health_status.get("cleanup_service", {}), "cleanup_service": health_status.get("cleanup_service", {}),
@ -322,6 +347,42 @@ async def health_check():
) )
@app.get("/ready")
async def readiness_check():
"""Kubernetes readiness probe - check if app can serve traffic"""
issues = []
# Check database
try:
from database.connection import check_db_connection, DATABASE_URL
if DATABASE_URL: # Only check if configured
if not check_db_connection():
issues.append("database_unavailable")
except ImportError:
pass # Database module not available - OK for development
except Exception as e:
issues.append(f"database_error: {str(e)}")
# Check Redis (optional but log if configured and unavailable)
if REDIS_URL:
redis_client = get_redis_client()
if redis_client:
try:
redis_client.ping()
except Exception:
issues.append("redis_unavailable")
elif redis_client is False:
issues.append("redis_connection_failed")
if issues:
return JSONResponse(
status_code=503,
content={"ready": False, "issues": issues}
)
return {"ready": True}
@app.get("/languages") @app.get("/languages")
async def get_supported_languages(): async def get_supported_languages():
"""Get list of supported language codes""" """Get list of supported language codes"""