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
This commit is contained in:
@@ -9,7 +9,9 @@ COPY requirements.txt .
|
|||||||
RUN pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
RUN chmod +x /app/docker/backend-entrypoint.sh
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/docker/backend-entrypoint.sh"]
|
||||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
|||||||
@@ -38,12 +38,17 @@ Run full stack (web + api + worker + postgres + redis + minio + mailpit):
|
|||||||
1. cp .env.docker.example .env
|
1. cp .env.docker.example .env
|
||||||
2. edit `.env` (`SECRET_KEY`, passwords, domain, `S3_PUBLIC_ENDPOINT_URL`)
|
2. edit `.env` (`SECRET_KEY`, passwords, domain, `S3_PUBLIC_ENDPOINT_URL`)
|
||||||
3. docker compose up -d --build
|
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
|
- Web: http://localhost
|
||||||
- API docs: http://localhost:8000/docs
|
- API docs: http://localhost:8000/docs
|
||||||
- Mailpit UI: http://localhost:8025
|
- Mailpit UI: http://localhost:8025
|
||||||
- MinIO console: http://localhost:9001
|
- MinIO console: http://localhost:9001
|
||||||
|
|
||||||
|
`RUN_MIGRATIONS_ON_STARTUP=true` (default in compose) runs `alembic upgrade head` before backend start.
|
||||||
|
|
||||||
### Production Mode
|
### Production Mode
|
||||||
|
|
||||||
Use production override to close internal ports (postgres/redis/minio/mailpit/backend):
|
Use production override to close internal ports (postgres/redis/minio/mailpit/backend):
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class Settings(BaseSettings):
|
|||||||
debug: bool = True
|
debug: bool = True
|
||||||
api_v1_prefix: str = "/api/v1"
|
api_v1_prefix: str = "/api/v1"
|
||||||
auto_create_tables: bool = True
|
auto_create_tables: bool = True
|
||||||
|
run_migrations_on_startup: bool = False
|
||||||
|
|
||||||
secret_key: str = Field(default="change-me-please-12345", min_length=16)
|
secret_key: str = Field(default="change-me-please-12345", min_length=16)
|
||||||
access_token_expire_minutes: int = 30
|
access_token_expire_minutes: int = 30
|
||||||
|
|||||||
35
app/main.py
35
app/main.py
@@ -1,6 +1,7 @@
|
|||||||
from contextlib import asynccontextmanager
|
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.auth.router import router as auth_router
|
||||||
from app.chats.router import router as chats_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.router import router as realtime_router
|
||||||
from app.realtime.service import realtime_gateway
|
from app.realtime.service import realtime_gateway
|
||||||
from app.users.router import router as users_router
|
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
|
@asynccontextmanager
|
||||||
@@ -36,6 +37,36 @@ async def health() -> dict[str, str]:
|
|||||||
return {"status": "ok"}
|
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(auth_router, prefix=settings.api_v1_prefix)
|
||||||
app.include_router(users_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)
|
app.include_router(chats_router, prefix=settings.api_v1_prefix)
|
||||||
|
|||||||
@@ -110,8 +110,14 @@ services:
|
|||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
environment:
|
environment:
|
||||||
<<: *app-env
|
<<: *app-env
|
||||||
|
RUN_MIGRATIONS_ON_STARTUP: ${RUN_MIGRATIONS_ON_STARTUP:-true}
|
||||||
ports:
|
ports:
|
||||||
- "${BACKEND_PORT:-8000}:8000"
|
- "${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:
|
worker:
|
||||||
build:
|
build:
|
||||||
@@ -127,6 +133,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
<<: *app-env
|
<<: *app-env
|
||||||
AUTO_CREATE_TABLES: false
|
AUTO_CREATE_TABLES: false
|
||||||
|
RUN_MIGRATIONS_ON_STARTUP: false
|
||||||
|
|
||||||
mailpit:
|
mailpit:
|
||||||
image: axllent/mailpit:latest
|
image: axllent/mailpit:latest
|
||||||
|
|||||||
12
docker/backend-entrypoint.sh
Normal file
12
docker/backend-entrypoint.sh
Normal file
@@ -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 "$@"
|
||||||
Reference in New Issue
Block a user