Enhance Docker deployment: structured logging, health checks, resource limits, entrypoint script
- Add structured logging config with JSON/text format support - Create docker-entrypoint.sh with dependency health checks - Enhance /health endpoint with DB and Redis connectivity checks - Add resource limits (CPU/memory) for all services - Add network aliases for service discovery - Add container hostnames for better identification - Reduce health check timeout from 20s to 5s - Add build metadata labels to Dockerfile - Use ENTRYPOINT for dependency checking before app startup - Log rotation: 50m per file, 5 files max
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
# MTG Online Backend - Docker Deployment
|
||||
|
||||
## Project Overview
|
||||
MTG Online backend REST API with FastAPI, SQLAlchemy, and Alembic migrations. Provides user authentication, deck management, card collection tracking, game rooms, and protocol message handling for MTG Online client compatibility.
|
||||
|
||||
## Docker Deployment Status
|
||||
|
||||
### Current State
|
||||
- **Docker Compose Stack**: Configured and tested
|
||||
- **Port Mapping**: 8990:8000 (backend exposed on host port 8990)
|
||||
- **Services**: 4 containers (backend, postgres, postgres_mtgdata, postgres_mirror, redis)
|
||||
- **Logging**: JSON file logging with rotation (10MB max, 3 files)
|
||||
|
||||
### Recent Fixes Applied
|
||||
1. **Database URL Fix**: Updated `.env` to use Docker service names instead of localhost
|
||||
- `postgres_mtgdata` for mtgdata database
|
||||
- `postgres` for mtgo_platform database
|
||||
- `postgres_mirror` for mtgo_mirror database
|
||||
|
||||
2. **Docker Build**: Successfully built backend image
|
||||
3. **Service Health**: All containers started and became healthy
|
||||
|
||||
### Known Issues
|
||||
- **Backend Connection Error**: Backend container couldn't connect to PostgreSQL due to localhost vs Docker service name mismatch
|
||||
- **Fix Applied**: Updated `.env` file with correct Docker service names
|
||||
- **Status**: Fix applied but NOT yet re-tested after restart
|
||||
|
||||
### Next Steps Required
|
||||
1. Restart Docker containers with updated `.env`
|
||||
2. Verify backend can connect to all three PostgreSQL databases
|
||||
3. Test health endpoint on port 8990
|
||||
4. Test authentication and CRUD operations
|
||||
5. Verify logging output
|
||||
|
||||
## Architecture
|
||||
- **Primary Database**: mtgo_platform (user data, decks, cards)
|
||||
- **MTG Data Database**: mtgdata (canonical card data)
|
||||
- **Mirror Database**: mtgo_mirror (fast deckbuilding queries)
|
||||
- **Cache**: Redis for session/cache management
|
||||
- **Async Driver**: asyncpg for async SQLAlchemy support
|
||||
|
||||
## Commands
|
||||
```bash
|
||||
# Start stack
|
||||
cd /home/wall-o/projects/mtgonline/backend
|
||||
docker compose up -d
|
||||
|
||||
# View logs
|
||||
docker compose logs -f backend
|
||||
docker compose logs -f postgres
|
||||
|
||||
# Stop stack
|
||||
docker compose down
|
||||
|
||||
# Rebuild after changes
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
## API Access
|
||||
- **Health Check**: http://localhost:8990/health
|
||||
- **API Docs**: http://localhost:8990/docs
|
||||
- **Backend Port**: 8990 (mapped to container port 8000)
|
||||
+25
-10
@@ -3,33 +3,45 @@ FROM python:3.12-slim as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install build dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
libpq-dev \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser
|
||||
|
||||
# Install Python dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir --user -r requirements.txt
|
||||
|
||||
# Application stage
|
||||
FROM python:3.12-slim
|
||||
|
||||
LABEL org.label-schema.name="mtgonline-backend" \
|
||||
org.label-schema.description="MTG Online Backend API" \
|
||||
org.label-schema.version="0.2.0" \
|
||||
org.label-schema.build-date="${BUILD_DATE}" \
|
||||
org.label-schema.vcs-url="https://gitea.wallomation.com/wall-o/mtgonline"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libpq5 \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy installed packages from builder
|
||||
COPY --from=builder /root/.local /app/.local
|
||||
|
||||
ENV PATH=/app/.local/bin:$PATH
|
||||
|
||||
# Create non-root user
|
||||
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser
|
||||
|
||||
RUN mkdir -p /app/data /app/uploads /app/logs /app/scripts /app/alembic/versions && chown -R appuser:appuser /app
|
||||
# Create necessary directories
|
||||
RUN mkdir -p /app/data /app/uploads /app/logs /app/scripts /app/alembic/versions && \
|
||||
chown -R appuser:appuser /app
|
||||
|
||||
# Copy application code
|
||||
COPY --chown=appuser:appuser app/ ./app/
|
||||
@@ -38,31 +50,34 @@ COPY --chown=appuser:appuser app/ ./app/
|
||||
COPY --chown=appuser:appuser alembic.ini ./
|
||||
COPY --chown=appuser:appuser alembic/ ./alembic/
|
||||
|
||||
# Copy interaction pipeline scripts
|
||||
# Copy scripts (including entrypoint)
|
||||
COPY --chown=appuser:appuser scripts/ ./scripts/
|
||||
RUN chmod +x /app/scripts/*.py
|
||||
RUN chmod +x /app/scripts/*.py /app/scripts/docker-entrypoint.sh
|
||||
|
||||
# Copy environment example
|
||||
COPY --chown=appuser:appuser .env.example ./.env.example
|
||||
COPY --chown=appuser:appuser pyproject.toml ./
|
||||
|
||||
# Python environment variables for proper logging
|
||||
# Python environment variables
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONPATH=/app
|
||||
|
||||
# Logging configuration
|
||||
ENV MTG_LOG_LEVEL=INFO
|
||||
ENV MTG_LOG_FORMAT=json
|
||||
ENV MTG_LOG_FORMAT=text
|
||||
|
||||
# Create logging directory with proper permissions
|
||||
RUN mkdir -p /app/logs && chown appuser:appuser /app/logs
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=20s --start-period=5s --retries=3 \
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
|
||||
CMD curl -f http://localhost:8000/health || exit 1
|
||||
|
||||
USER appuser
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
# Run migrations before starting the app
|
||||
CMD ["sh", "-c", "python -m alembic upgrade head && python -m uvicorn app.main:app --host 0.0.0.0 --port 8000"]
|
||||
# Use entrypoint script for dependency checking and migrations
|
||||
ENTRYPOINT ["/app/scripts/docker-entrypoint.sh"]
|
||||
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--forwarded-allow-ips", "*"]
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
Structured logging configuration for MTG Online backend.
|
||||
|
||||
Supports JSON and text log formats with container metadata.
|
||||
Configured via MTG_LOG_LEVEL and MTG_LOG_FORMAT environment variables.
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
def _get_container_name() -> str:
|
||||
"""Get container name from HOSTNAME env var or fallback."""
|
||||
return os.environ.get("HOSTNAME", "unknown")
|
||||
|
||||
|
||||
class ContainerFormatter(logging.Formatter):
|
||||
"""Formatter that adds container metadata to log records.
|
||||
|
||||
Supports both text and JSON output formats.
|
||||
"""
|
||||
|
||||
SERVICE_NAME = "mtgonline-backend"
|
||||
|
||||
def __init__(self, fmt_type: str = "text"):
|
||||
super().__init__()
|
||||
self.fmt_type = fmt_type
|
||||
self.container_name = _get_container_name()
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
record.service = self.SERVICE_NAME
|
||||
record.container = self.container_name
|
||||
|
||||
if self.fmt_type == "json":
|
||||
return self._format_json(record)
|
||||
return self._format_text(record)
|
||||
|
||||
def _format_text(self, record: logging.LogRecord) -> str:
|
||||
return (
|
||||
f"{self.formatTime(record)} [{record.service}] "
|
||||
f"{record.levelname} - {record.getMessage()}"
|
||||
)
|
||||
|
||||
def _format_json(self, record: logging.LogRecord) -> str:
|
||||
log_entry = {
|
||||
"timestamp": datetime.fromtimestamp(
|
||||
record.created, tz=timezone.utc
|
||||
).isoformat(),
|
||||
"level": record.levelname,
|
||||
"service": record.service,
|
||||
"container": record.container,
|
||||
"logger": record.name,
|
||||
"message": record.getMessage(),
|
||||
}
|
||||
if record.exc_info and record.exc_info[0] is not None:
|
||||
log_entry["exception"] = self.formatException(record.exc_info)
|
||||
return json.dumps(log_entry, default=str)
|
||||
|
||||
|
||||
def configure_logging() -> None:
|
||||
"""Configure application logging based on environment variables.
|
||||
|
||||
Reads:
|
||||
MTG_LOG_LEVEL - Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
MTG_LOG_FORMAT - Output format ("text" or "json")
|
||||
"""
|
||||
log_level = os.environ.get("MTG_LOG_LEVEL", "INFO").upper()
|
||||
log_format = os.environ.get("MTG_LOG_FORMAT", "text").lower()
|
||||
|
||||
level = getattr(logging, log_level, logging.INFO)
|
||||
|
||||
formatter = ContainerFormatter(fmt_type=log_format)
|
||||
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(level)
|
||||
root_logger.handlers.clear()
|
||||
root_logger.addHandler(handler)
|
||||
|
||||
# Suppress noisy loggers unless DEBUG
|
||||
if level != logging.DEBUG:
|
||||
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
|
||||
logging.getLogger("sqlalchemy.pool").setLevel(logging.WARNING)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(
|
||||
f"Logging configured: level={log_level}, "
|
||||
f"format={log_format}, container={_get_container_name()}"
|
||||
)
|
||||
|
||||
|
||||
# Auto-configure on import
|
||||
configure_logging()
|
||||
+37
-33
@@ -21,32 +21,14 @@ from typing import AsyncGenerator
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.core.settings import get_settings
|
||||
from app.core.database import engine, mtg_engine, async_session, mtg_async_session
|
||||
from app.routers import auth, users, decks, rooms, admin, card_router, interactions, refresh, user_data, card_import
|
||||
from app.routers.games import router as games_router
|
||||
from app.services.mtgjson_manager import MTGJSONManager
|
||||
|
||||
|
||||
def setup_logging(debug: bool = False) -> None:
|
||||
"""Configure application logging with verbose support."""
|
||||
level = logging.DEBUG if debug else logging.INFO
|
||||
|
||||
logging.basicConfig(
|
||||
level=level,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(),
|
||||
]
|
||||
)
|
||||
|
||||
if debug:
|
||||
logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING)
|
||||
logging.getLogger('sqlalchemy.pool').setLevel(logging.WARNING)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"Logging initialized at level {logging.getLevelName(level)}")
|
||||
from app.core.logging_config import configure_logging
|
||||
|
||||
|
||||
async def run_initial_download():
|
||||
@@ -91,8 +73,8 @@ def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
"""Application lifespan events for startup and shutdown."""
|
||||
settings = get_settings()
|
||||
|
||||
# Startup
|
||||
setup_logging(debug=settings.DEBUG)
|
||||
# Configure logging
|
||||
configure_logging()
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"MTG Online Backend starting (v{settings.APP_VERSION})")
|
||||
logger.info(f"Database: {settings.DATABASE_URL.split('@')[1] if '@' in settings.DATABASE_URL else 'configured'}")
|
||||
@@ -146,24 +128,46 @@ app.include_router(card_import.router, prefix="/api/v1/card-import", tags=["Card
|
||||
|
||||
@app.get("/health", tags=["Health"])
|
||||
async def health_check():
|
||||
"""Health check endpoint with MTGJSON data status."""
|
||||
"""Health check endpoint with full service status."""
|
||||
from app.services.mtgjson_manager import get_manager
|
||||
|
||||
# Get MTGJSON health status
|
||||
health_status = {"status": "healthy", "version": settings.APP_VERSION}
|
||||
|
||||
# Check MTGJSON status
|
||||
try:
|
||||
manager = get_manager()
|
||||
mtg_status = await manager.get_health_status()
|
||||
health_status["mtgjson"] = mtg_status
|
||||
if mtg_status.get("status") != "healthy":
|
||||
health_status["status"] = "degraded"
|
||||
except Exception as e:
|
||||
mtg_status = {
|
||||
"status": "unhealthy",
|
||||
"error": str(e),
|
||||
}
|
||||
health_status["mtgjson"] = {"status": "unhealthy", "error": str(e)}
|
||||
health_status["status"] = "degraded"
|
||||
|
||||
return {
|
||||
"status": "healthy" if mtg_status.get("status") == "healthy" else "degraded",
|
||||
"version": settings.APP_VERSION,
|
||||
"mtgjson": mtg_status,
|
||||
}
|
||||
# Check primary database
|
||||
try:
|
||||
from app.core.database import engine
|
||||
async with engine.connect() as conn:
|
||||
await conn.execute(text("SELECT 1"))
|
||||
health_status["database"] = "healthy"
|
||||
except Exception as e:
|
||||
health_status["database"] = {"status": "unhealthy", "error": str(e)}
|
||||
health_status["status"] = "unhealthy"
|
||||
|
||||
# Check Redis
|
||||
try:
|
||||
from app.core.redis_client import get_redis
|
||||
redis_client = await get_redis()
|
||||
if redis_client:
|
||||
health_status["redis"] = "healthy"
|
||||
else:
|
||||
health_status["redis"] = {"status": "unhealthy", "error": "Connection failed"}
|
||||
health_status["status"] = "unhealthy"
|
||||
except Exception as e:
|
||||
health_status["redis"] = {"status": "unhealthy", "error": str(e)}
|
||||
health_status["status"] = "unhealthy"
|
||||
|
||||
return health_status
|
||||
|
||||
|
||||
@app.get("/", tags=["Root"])
|
||||
|
||||
+104
-17
@@ -3,6 +3,8 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: mtgonline_backend
|
||||
hostname: mtgonline_backend
|
||||
ports:
|
||||
- "8990:8000"
|
||||
environment:
|
||||
@@ -16,7 +18,7 @@ services:
|
||||
- MTG_INTERACTION_BATCH_SIZE=1000
|
||||
- MTG_INTERACTION_REVIEW_QUEUE=true
|
||||
- MTG_LOG_LEVEL=INFO
|
||||
- MTG_LOG_FORMAT=json
|
||||
- MTG_LOG_FORMAT=text
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
- SECRET_KEY=change-this-secret-key-in-production
|
||||
- JWT_SECRET_KEY=change-this-jwt-secret-key-in-production
|
||||
@@ -38,42 +40,72 @@ services:
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||
interval: 30s
|
||||
timeout: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
max-size: "50m"
|
||||
max-file: "5"
|
||||
tag: "{{.Name}}"
|
||||
networks:
|
||||
- mtgonline_network
|
||||
mtgonline_network:
|
||||
aliases:
|
||||
- backend
|
||||
- mtgonline-api
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1G
|
||||
cpus: "1.0"
|
||||
reservations:
|
||||
memory: 512M
|
||||
cpus: "0.5"
|
||||
|
||||
postgres_platform:
|
||||
image: postgres:16-alpine
|
||||
container_name: mtgonline_postgres_platform
|
||||
hostname: postgres_platform
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DB=mtgo_platform
|
||||
volumes:
|
||||
- postgres_platform_data:/var/lib/postgresql/data
|
||||
- ./scripts/init_platform.sql:/docker-entrypoint-initdb.d/01-init.sql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d mtgo_platform"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
max-size: "50m"
|
||||
max-file: "5"
|
||||
tag: "{{.Name}}"
|
||||
networks:
|
||||
- mtgonline_network
|
||||
mtgonline_network:
|
||||
aliases:
|
||||
- postgres-platform
|
||||
- platform-db
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
cpus: "0.5"
|
||||
reservations:
|
||||
memory: 256M
|
||||
cpus: "0.25"
|
||||
|
||||
postgres_mtgdata:
|
||||
image: postgres:16-alpine
|
||||
container_name: mtgonline_postgres_mtgdata
|
||||
hostname: postgres_mtgdata
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
@@ -85,17 +117,32 @@ services:
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
max-size: "50m"
|
||||
max-file: "5"
|
||||
tag: "{{.Name}}"
|
||||
networks:
|
||||
- mtgonline_network
|
||||
mtgonline_network:
|
||||
aliases:
|
||||
- postgres-mtgdata
|
||||
- mtgdata-db
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
cpus: "0.5"
|
||||
reservations:
|
||||
memory: 256M
|
||||
cpus: "0.25"
|
||||
|
||||
postgres_mirror:
|
||||
image: postgres:16-alpine
|
||||
container_name: mtgonline_postgres_mirror
|
||||
hostname: postgres_mirror
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
@@ -107,39 +154,79 @@ services:
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
max-size: "50m"
|
||||
max-file: "5"
|
||||
tag: "{{.Name}}"
|
||||
networks:
|
||||
- mtgonline_network
|
||||
mtgonline_network:
|
||||
aliases:
|
||||
- postgres-mirror
|
||||
- mirror-db
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
cpus: "0.5"
|
||||
reservations:
|
||||
memory: 256M
|
||||
cpus: "0.25"
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: mtgonline_redis
|
||||
hostname: redis
|
||||
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
max-size: "50m"
|
||||
max-file: "5"
|
||||
tag: "{{.Name}}"
|
||||
networks:
|
||||
- mtgonline_network
|
||||
mtgonline_network:
|
||||
aliases:
|
||||
- redis
|
||||
- cache
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
cpus: "0.5"
|
||||
reservations:
|
||||
memory: 256M
|
||||
cpus: "0.25"
|
||||
|
||||
volumes:
|
||||
postgres_platform_data:
|
||||
driver: local
|
||||
postgres_mtgdata_data:
|
||||
driver: local
|
||||
postgres_mirror_data:
|
||||
driver: local
|
||||
mtg_data:
|
||||
driver: local
|
||||
mtg_uploads:
|
||||
driver: local
|
||||
mtg_logs:
|
||||
driver: local
|
||||
redis_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
mtgonline_network:
|
||||
driver: bridge
|
||||
name: mtgonline_network
|
||||
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
echo "============================================"
|
||||
echo " MTG Online Backend - Starting"
|
||||
echo " $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
||||
echo "============================================"
|
||||
|
||||
# Wait for PostgreSQL to be ready
|
||||
echo "[entrypoint] Waiting for PostgreSQL databases..."
|
||||
for db in mtgo_platform mtgdata mtgo_mirror; do
|
||||
host="postgres_${db}"
|
||||
echo " Checking ${host}..."
|
||||
until python -c "
|
||||
import asyncio, asyncpg
|
||||
async def check():
|
||||
try:
|
||||
conn = await asyncpg.connect(host='${host}', port=5432, user='postgres', password='postgres', database='${db}')
|
||||
await conn.close()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
asyncio.run(check())
|
||||
" 2>/dev/null; do
|
||||
echo " ${host} not ready, waiting 2s..."
|
||||
sleep 2
|
||||
done
|
||||
echo " ${host} is ready!"
|
||||
done
|
||||
|
||||
# Wait for Redis to be ready
|
||||
echo "[entrypoint] Waiting for Redis..."
|
||||
until python -c "
|
||||
import asyncio, redis.asyncio as aioredis
|
||||
async def check():
|
||||
try:
|
||||
r = aioredis.from_url('redis://redis:6379/0')
|
||||
await r.ping()
|
||||
await r.close()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
asyncio.run(check())
|
||||
" 2>/dev/null; do
|
||||
echo " Redis not ready, waiting 2s..."
|
||||
sleep 2
|
||||
done
|
||||
echo " Redis is ready!"
|
||||
|
||||
# Run database migrations
|
||||
echo "[entrypoint] Running Alembic migrations..."
|
||||
python -m alembic upgrade head
|
||||
echo "[entrypoint] Migrations complete."
|
||||
|
||||
# Start the application
|
||||
echo "[entrypoint] Starting uvicorn..."
|
||||
exec python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --forwarded-allow-ips '*' --log-level info
|
||||
Executable
+236
@@ -0,0 +1,236 @@
|
||||
#!/bin/bash
|
||||
# MTG Online Backend - Docker Deployment Helper Scripts
|
||||
# ============================================================
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Project root directory
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE} MTG Online Backend - Docker Deployment${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
|
||||
# Function to display usage
|
||||
usage() {
|
||||
echo "Usage: $0 <command>"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " start - Start all services"
|
||||
echo " stop - Stop all services"
|
||||
echo " restart - Restart all services"
|
||||
echo " logs - View logs (follow mode)"
|
||||
echo " logs-app - View application logs only"
|
||||
echo " logs-db - View database logs only"
|
||||
echo " status - Show service status"
|
||||
echo " health - Check health of all services"
|
||||
echo " exec-app - Execute command in backend container"
|
||||
echo " exec-db - Execute command in database container"
|
||||
echo " migrate - Run database migrations"
|
||||
echo " cleanup - Stop and remove all containers and volumes"
|
||||
echo " rebuild - Rebuild and start all services"
|
||||
echo " help - Show this help message"
|
||||
echo ""
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Function to check if docker is running
|
||||
check_docker() {
|
||||
if ! docker info > /dev/null 2>&1; then
|
||||
echo -e "${RED}Error: Docker is not running${NC}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to check if docker-compose is available
|
||||
check_compose() {
|
||||
if ! command -v docker-compose &> /dev/null; then
|
||||
echo -e "${RED}Error: docker-compose is not installed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to start services
|
||||
start_services() {
|
||||
echo -e "${GREEN}Starting MTG Online Backend services...${NC}"
|
||||
cd "$PROJECT_ROOT/backend"
|
||||
docker-compose up -d
|
||||
echo -e "${GREEN}✓ Services started successfully${NC}"
|
||||
echo ""
|
||||
echo -e "${BLUE}Access the application at: http://localhost:8990${NC}"
|
||||
echo -e "${BLUE}View logs with: $0 logs${NC}"
|
||||
}
|
||||
|
||||
# Function to stop services
|
||||
stop_services() {
|
||||
echo -e "${YELLOW}Stopping MTG Online Backend services...${NC}"
|
||||
cd "$PROJECT_ROOT/backend"
|
||||
docker-compose down
|
||||
echo -e "${GREEN}✓ Services stopped${NC}"
|
||||
}
|
||||
|
||||
# Function to restart services
|
||||
restart_services() {
|
||||
echo -e "${YELLOW}Restarting MTG Online Backend services...${NC}"
|
||||
cd "$PROJECT_ROOT/backend"
|
||||
docker-compose restart
|
||||
echo -e "${GREEN}✓ Services restarted${NC}"
|
||||
}
|
||||
|
||||
# Function to view logs
|
||||
view_logs() {
|
||||
cd "$PROJECT_ROOT/backend"
|
||||
docker-compose logs -f --tail=100
|
||||
}
|
||||
|
||||
# Function to view application logs only
|
||||
view_app_logs() {
|
||||
cd "$PROJECT_ROOT/backend"
|
||||
docker-compose logs -f --tail=100 backend
|
||||
}
|
||||
|
||||
# Function to view database logs only
|
||||
view_db_logs() {
|
||||
cd "$PROJECT_ROOT/backend"
|
||||
docker-compose logs -f --tail=100 postgres_platform postgres_mtgdata postgres_mirror
|
||||
}
|
||||
|
||||
# Function to show status
|
||||
show_status() {
|
||||
cd "$PROJECT_ROOT/backend"
|
||||
echo -e "${BLUE}Service Status:${NC}"
|
||||
docker-compose ps
|
||||
}
|
||||
|
||||
# Function to check health
|
||||
check_health() {
|
||||
cd "$PROJECT_ROOT/backend"
|
||||
echo -e "${BLUE}Checking service health...${NC}"
|
||||
docker-compose exec -T backend curl -s http://localhost:8000/health | python3 -m json.tool
|
||||
echo ""
|
||||
echo -e "${BLUE}Database Health:${NC}"
|
||||
docker-compose exec -T postgres_platform pg_isready -U postgres
|
||||
docker-compose exec -T postgres_mtgdata pg_isready -U postgres
|
||||
docker-compose exec -T postgres_mirror pg_isready -U postgres
|
||||
echo ""
|
||||
echo -e "${BLUE}Redis Health:${NC}"
|
||||
docker-compose exec -T redis redis-cli ping
|
||||
}
|
||||
|
||||
# Function to execute command in backend container
|
||||
exec_app() {
|
||||
cd "$PROJECT_ROOT/backend"
|
||||
docker-compose exec backend "$@"
|
||||
}
|
||||
|
||||
# Function to execute command in database container
|
||||
exec_db() {
|
||||
local db_name=$1
|
||||
shift
|
||||
cd "$PROJECT_ROOT/backend"
|
||||
docker-compose exec "$db_name" psql -U postgres "$@"
|
||||
}
|
||||
|
||||
# Function to run migrations
|
||||
run_migrations() {
|
||||
echo -e "${YELLOW}Running database migrations...${NC}"
|
||||
cd "$PROJECT_ROOT/backend"
|
||||
docker-compose exec backend python -m alembic upgrade head
|
||||
echo -e "${GREEN}✓ Migrations completed${NC}"
|
||||
}
|
||||
|
||||
# Function to cleanup
|
||||
cleanup() {
|
||||
echo -e "${RED}WARNING: This will stop and remove all containers and volumes!${NC}"
|
||||
read -p "Are you sure? (y/N) " -n 1 -r
|
||||
echo ""
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
cd "$PROJECT_ROOT/backend"
|
||||
docker-compose down -v
|
||||
echo -e "${GREEN}✓ All containers and volumes removed${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to rebuild
|
||||
rebuild() {
|
||||
echo -e "${YELLOW}Rebuilding and starting all services...${NC}"
|
||||
cd "$PROJECT_ROOT/backend"
|
||||
docker-compose down
|
||||
docker-compose build --no-cache
|
||||
docker-compose up -d
|
||||
echo -e "${GREEN}✓ Services rebuilt and started${NC}"
|
||||
echo ""
|
||||
echo -e "${BLUE}Access the application at: http://localhost:8990${NC}"
|
||||
}
|
||||
|
||||
# Main command handler
|
||||
case "${1:-help}" in
|
||||
start)
|
||||
check_docker
|
||||
start_services
|
||||
;;
|
||||
stop)
|
||||
check_docker
|
||||
stop_services
|
||||
;;
|
||||
restart)
|
||||
check_docker
|
||||
restart_services
|
||||
;;
|
||||
logs)
|
||||
check_docker
|
||||
view_logs
|
||||
;;
|
||||
logs-app)
|
||||
check_docker
|
||||
view_app_logs
|
||||
;;
|
||||
logs-db)
|
||||
check_docker
|
||||
view_db_logs
|
||||
;;
|
||||
status)
|
||||
check_docker
|
||||
show_status
|
||||
;;
|
||||
health)
|
||||
check_docker
|
||||
check_health
|
||||
;;
|
||||
exec-app)
|
||||
check_docker
|
||||
shift
|
||||
exec_app "$@"
|
||||
;;
|
||||
exec-db)
|
||||
check_docker
|
||||
exec_db "$@"
|
||||
;;
|
||||
migrate)
|
||||
check_docker
|
||||
run_migrations
|
||||
;;
|
||||
cleanup)
|
||||
check_docker
|
||||
cleanup
|
||||
;;
|
||||
rebuild)
|
||||
check_docker
|
||||
rebuild
|
||||
;;
|
||||
help|--help|-h)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unknown command: $1${NC}"
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user