Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jwt_algorithm=HS256
# cache before it's stored in Redis (see app/core/securite.py:
# encrypt_refresh_cache_payload / decrypt_refresh_cache_payload).
# Must be a base64-encoded 32-byte (256-bit) key.
encryption_key=
encryption_key=Oy/A1P2ziik16x7dCzb2sBYOMLh6aol6MVn2cyPifag=

totp_issuer=MultiAI

Expand Down
2 changes: 1 addition & 1 deletion .env.staging.example
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ PGADMIN_PORT=5050

jwt_secret=super_secret_jwt_key
jwt_algorithm=HS256
encryption_key=super_secret_encryption_key
encryption_key=Oy/A1P2ziik16x7dCzb2sBYOMLh6aol6MVn2cyPifag=
totp_issuer=MultiAI


Expand Down
27 changes: 24 additions & 3 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,30 @@ def read_root() -> dict[str, str]:
return {"Hello": "World"}


@app.get("/health")
def health_check() -> dict[str, str]:
return {"status": "healthy"}
@app.get("/health", tags=["ops"])
async def health_check(response: Response) -> dict:
"""Liveness + readiness probe. Returns 503 if Postgres or Redis is unreachable."""
from sqlalchemy import text
from app.infra.redis import RedisClient

errors: list[str] = []
try:
async with engine.connect() as conn:
await conn.execute(text("SELECT 1"))
except Exception:
errors.append("postgres")

try:
# We need to call .ping() on the underlying redis-py client
await RedisClient.get_instance()._client.ping() # type: ignore[misc]
except Exception as e:
logger.warning(f"Healthcheck Redis failed: {e}")
errors.append("redis")

if errors:
response.status_code = 503
return {"status": "unhealthy", "failing": errors}
return {"status": "ok"}


app.include_router(mobile_router)
Expand Down
118 changes: 85 additions & 33 deletions docker-compose.staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ services:
- postgres_data:/var/lib/postgresql/data
networks:
- multi_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s

nats:
image: nats:2.10-alpine
Expand All @@ -35,6 +41,12 @@ services:
- nats_data:/data
networks:
- multi_network
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://localhost:${NATS_MONITOR_PORT}/healthz || exit 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s

minio:
image: minio/minio:latest
Expand All @@ -50,18 +62,12 @@ services:
- minio_data:/data
networks:
- multi_network

pgadmin:
image: dpage/pgadmin4
container_name: multi_pgadmin
restart: unless-stopped
environment:
PGADMIN_DEFAULT_EMAIL: admin@example.com
PGADMIN_DEFAULT_PASSWORD: admin
ports:
- "${PGADMIN_PORT}:80"
networks:
- multi_network
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:9000/minio/health/live || exit 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s

redis:
image: redis:7-alpine
Expand All @@ -75,6 +81,12 @@ services:
- multi_network
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s

fastapi:
image: ghcr.io/microclub-usthb/multai-back:latest
Expand All @@ -83,22 +95,46 @@ services:
env_file:
- .env.staging
depends_on:
- postgres
- redis
- nats
- minio
postgres:
condition: service_healthy
redis:
condition: service_healthy
nats:
condition: service_healthy
minio:
condition: service_healthy
ports:
- "8000:8000"
networks:
- multi_network
healthcheck:
# Uses Python's stdlib — no curl needed in python:3.12-slim.
# 60s start_period accounts for AI model loading at boot.
test:
- CMD
- python
- -c
- |
import urllib.request, sys
try:
r = urllib.request.urlopen('http://localhost:8000/health', timeout=4)
sys.exit(0 if r.status == 200 else 1)
except Exception:
sys.exit(1)
interval: 15s
timeout: 5s
retries: 5
start_period: 60s

migrate:
image: ghcr.io/microclub-usthb/multai-back:latest
container_name: multi_migrate
restart: "no"
env_file:
- .env.staging
depends_on:
- postgres
postgres:
condition: service_healthy
command: ["uv", "run", "alembic", "upgrade", "head"]
networks:
- multi_network
Expand All @@ -110,8 +146,8 @@ services:
env_file:
- .env.staging
depends_on:
- nats
- redis
nats:
condition: service_healthy
command: ["uv", "run", "python", "-m", "app.worker.email_worker.main"]
networks:
- multi_network
Expand All @@ -123,10 +159,14 @@ services:
env_file:
- .env.staging
depends_on:
- postgres
- redis
- nats
- minio
postgres:
condition: service_healthy
redis:
condition: service_healthy
nats:
condition: service_healthy
minio:
condition: service_healthy
command: ["uv", "run", "python", "-m", "app.worker.photo_worker.main"]
networks:
- multi_network
Expand All @@ -139,8 +179,10 @@ services:
env_file:
- .env.staging
depends_on:
- nats
- redis
nats:
condition: service_healthy
redis:
condition: service_healthy
command: ["uv", "run", "python", "-m", "app.worker.notification.main"]
networks:
- multi_network
Expand All @@ -152,10 +194,14 @@ services:
env_file:
- .env.staging
depends_on:
- postgres
- redis
- nats
- minio
postgres:
condition: service_healthy
redis:
condition: service_healthy
nats:
condition: service_healthy
minio:
condition: service_healthy
command: ["uv", "run", "python", "-m", "app.worker.upload_group_worker.main"]
networks:
- multi_network
Expand All @@ -167,8 +213,10 @@ services:
env_file:
- .env.staging
depends_on:
- postgres
- nats
postgres:
condition: service_healthy
nats:
condition: service_healthy
command: ["uv", "run", "python", "-m", "app.worker.audit.main"]
networks:
- multi_network
Expand All @@ -180,8 +228,12 @@ services:
env_file:
- .env.staging
depends_on:
- postgres
- nats
postgres:
condition: service_healthy
nats:
condition: service_healthy
minio:
condition: service_healthy
command: ["uv", "run", "python", "-m", "app.worker.storage_cleaner.main"]
networks:
- multi_network
Expand Down
6 changes: 5 additions & 1 deletion makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ ifneq ("$(wildcard .env)","")
export
endif

.PHONY: migration-create m-up m-down gen get_db run-app run-workers lint staging-check-up staging-check-logs staging-check-down
.PHONY: dev migration-create m-up m-down gen get_db run-app run-workers lint staging-check-up staging-check-logs staging-check-down

# Helper variable to call your new cleaning script
CLEAN_SCHEMA = uv run python scripts/clean_schema.py db/schema.sql
Expand Down Expand Up @@ -81,3 +81,7 @@ staging-check-logs:

staging-check-down:
docker compose -f docker-compose.staging.yml -f docker-compose.staging.local.yml down

dev:
docker compose up -d
$(MAKE) -j 2 run-app run-workers
27 changes: 27 additions & 0 deletions scripts/seed_admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import asyncio
from app.infra.database import engine
from app.container import Container
from db.generated.models import StaffRole
from app.infra.redis import RedisClient

async def main():
RedisClient.init(host="localhost", port=6379, password="")
async with engine.begin() as conn:
container = Container(conn)

# Check if exists
existing = await container.staff_user_service.staff_user_querier.get_staff_user_by_email(email="m@example.com")
if existing:
print("Admin already exists!")
return

print("Creating admin user m@example.com...")
await container.staff_user_service.create_staff_user(
email="m@example.com",
password="password",
role=StaffRole.ADMIN
)
print("Admin user created! password is: password")

if __name__ == "__main__":
asyncio.run(main())