From df79a70baf4beaef3d552a9e44dbd8bc375e99f9 Mon Sep 17 00:00:00 2001 From: benya Date: Sun, 8 Mar 2026 02:50:57 +0300 Subject: [PATCH] chore(prod): startup migrations, readiness checks and backend healthcheck - add backend entrypoint that can run alembic upgrade head on startup - add RUN_MIGRATIONS_ON_STARTUP setting and compose wiring - add /health/live and /health/ready endpoints with db+redis checks - add backend container healthcheck against readiness endpoint - document readiness and startup migration behavior --- Dockerfile | 2 ++ README.md | 7 ++++++- app/config/settings.py | 1 + app/main.py | 35 +++++++++++++++++++++++++++++++++-- docker-compose.yml | 7 +++++++ docker/backend-entrypoint.sh | 12 ++++++++++++ 6 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 docker/backend-entrypoint.sh diff --git a/Dockerfile b/Dockerfile index 90bccca..c5d5fff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,9 @@ COPY requirements.txt . RUN pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir -r requirements.txt COPY . . +RUN chmod +x /app/docker/backend-entrypoint.sh EXPOSE 8000 +ENTRYPOINT ["/app/docker/backend-entrypoint.sh"] CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/README.md b/README.md index 2000fdd..889c6fa 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,17 @@ Run full stack (web + api + worker + postgres + redis + minio + mailpit): 1. cp .env.docker.example .env 2. edit `.env` (`SECRET_KEY`, passwords, domain, `S3_PUBLIC_ENDPOINT_URL`) 3. docker compose up -d --build -2. Open: +4. check backend readiness: + - `http://localhost:8000/health/live` + - `http://localhost:8000/health/ready` +5. Open: - Web: http://localhost - API docs: http://localhost:8000/docs - Mailpit UI: http://localhost:8025 - MinIO console: http://localhost:9001 +`RUN_MIGRATIONS_ON_STARTUP=true` (default in compose) runs `alembic upgrade head` before backend start. + ### Production Mode Use production override to close internal ports (postgres/redis/minio/mailpit/backend): diff --git a/app/config/settings.py b/app/config/settings.py index b04768f..8119da9 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -8,6 +8,7 @@ class Settings(BaseSettings): debug: bool = True api_v1_prefix: str = "/api/v1" auto_create_tables: bool = True + run_migrations_on_startup: bool = False secret_key: str = Field(default="change-me-please-12345", min_length=16) access_token_expire_minutes: int = 30 diff --git a/app/main.py b/app/main.py index 19846ca..ed32b3a 100644 --- a/app/main.py +++ b/app/main.py @@ -1,6 +1,7 @@ from contextlib import asynccontextmanager -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException, status +from sqlalchemy import text from app.auth.router import router as auth_router from app.chats.router import router as chats_router @@ -14,7 +15,7 @@ from app.notifications.router import router as notifications_router from app.realtime.router import router as realtime_router from app.realtime.service import realtime_gateway from app.users.router import router as users_router -from app.utils.redis_client import close_redis_client +from app.utils.redis_client import close_redis_client, get_redis_client @asynccontextmanager @@ -36,6 +37,36 @@ async def health() -> dict[str, str]: return {"status": "ok"} +@app.get("/health/live", tags=["health"]) +async def health_live() -> dict[str, str]: + return {"status": "ok"} + + +@app.get("/health/ready", tags=["health"]) +async def health_ready() -> dict[str, str]: + db_ok = False + redis_ok = False + try: + async with engine.connect() as conn: + await conn.execute(text("SELECT 1")) + db_ok = True + except Exception: + db_ok = False + try: + redis = get_redis_client() + pong = await redis.ping() + redis_ok = bool(pong) + except Exception: + redis_ok = False + + if not db_ok or not redis_ok: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail={"status": "not_ready", "db": db_ok, "redis": redis_ok}, + ) + return {"status": "ready", "db": "ok", "redis": "ok"} + + app.include_router(auth_router, prefix=settings.api_v1_prefix) app.include_router(users_router, prefix=settings.api_v1_prefix) app.include_router(chats_router, prefix=settings.api_v1_prefix) diff --git a/docker-compose.yml b/docker-compose.yml index 8389fb4..00ded13 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -110,8 +110,14 @@ services: condition: service_completed_successfully environment: <<: *app-env + RUN_MIGRATIONS_ON_STARTUP: ${RUN_MIGRATIONS_ON_STARTUP:-true} ports: - "${BACKEND_PORT:-8000}:8000" + healthcheck: + test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/health/ready').read()\""] + interval: 10s + timeout: 5s + retries: 12 worker: build: @@ -127,6 +133,7 @@ services: environment: <<: *app-env AUTO_CREATE_TABLES: false + RUN_MIGRATIONS_ON_STARTUP: false mailpit: image: axllent/mailpit:latest diff --git a/docker/backend-entrypoint.sh b/docker/backend-entrypoint.sh new file mode 100644 index 0000000..fe5252b --- /dev/null +++ b/docker/backend-entrypoint.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env sh +set -eu + +echo "[entrypoint] starting backend container" + +if [ "${RUN_MIGRATIONS_ON_STARTUP:-false}" = "true" ]; then + echo "[entrypoint] running alembic migrations" + alembic upgrade head +fi + +echo "[entrypoint] launching application" +exec "$@"