diff --git a/docker/backend/Dockerfile b/docker/backend/Dockerfile index 0f15ca8..0769242 100644 --- a/docker/backend/Dockerfile +++ b/docker/backend/Dockerfile @@ -9,6 +9,7 @@ WORKDIR /app RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ libmagic1 \ + libpq-dev \ && rm -rf /var/lib/apt/lists/* # Copy requirements first for better caching @@ -28,6 +29,7 @@ WORKDIR /app # Install runtime dependencies only RUN apt-get update && apt-get install -y --no-install-recommends \ libmagic1 \ + libpq5 \ curl \ && rm -rf /var/lib/apt/lists/* \ && apt-get clean @@ -46,20 +48,24 @@ RUN mkdir -p /app/uploads /app/outputs /app/logs /app/temp \ # Copy application code COPY --chown=translator:translator . . +# Make entrypoint executable +RUN chmod +x docker/backend/entrypoint.sh + # Switch to non-root user USER translator # Environment variables ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ - PORT=8000 + PORT=8000 \ + WORKERS=4 # 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 # Expose port EXPOSE ${PORT} -# Run with uvicorn -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"] +# Use entrypoint script to handle migrations and startup +ENTRYPOINT ["docker/backend/entrypoint.sh"] diff --git a/docker/backend/entrypoint.sh b/docker/backend/entrypoint.sh new file mode 100644 index 0000000..cc54a06 --- /dev/null +++ b/docker/backend/entrypoint.sh @@ -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} diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index 7a1bffd..f7a7cb6 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -25,6 +25,7 @@ data: RATE_LIMIT_REQUESTS_PER_MINUTE: "60" RATE_LIMIT_TRANSLATIONS_PER_MINUTE: "10" LOG_LEVEL: "INFO" + # Database and Redis URLs are in secrets for security --- # Secret for sensitive data @@ -35,10 +36,17 @@ metadata: namespace: translate-api type: Opaque stringData: + # CHANGE ALL THESE IN PRODUCTION! 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: "" OPENAI_API_KEY: "" + OPENROUTER_API_KEY: "" + STRIPE_SECRET_KEY: "" + STRIPE_WEBHOOK_SECRET: "" --- # Backend Deployment @@ -79,16 +87,18 @@ spec: cpu: "1000m" readinessProbe: httpGet: - path: /health + path: /ready port: 8000 initialDelaySeconds: 10 periodSeconds: 10 + failureThreshold: 3 livenessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 30 periodSeconds: 30 + failureThreshold: 3 volumeMounts: - name: uploads mountPath: /app/uploads @@ -286,3 +296,189 @@ spec: target: type: Utilization 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 diff --git a/main.py b/main.py index a4895c4..c70c98d 100644 --- a/main.py +++ b/main.py @@ -305,11 +305,36 @@ async def health_check(): health_status = await health_checker.check_health() 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( status_code=status_code, content={ "status": health_status.get("status", "unknown"), "translation_service": config.TRANSLATION_SERVICE, + "database": db_status, + "redis": redis_status, "memory": health_status.get("memory", {}), "disk": health_status.get("disk", {}), "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") async def get_supported_languages(): """Get list of supported language codes"""