diff --git a/backend/Dockerfile b/backend/Dockerfile index 96ef064..28c262f 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -11,5 +11,6 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . -# Tables are created at runtime by SQLAlchemy in app/main.py lifespan -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"] +# Single worker avoids race condition during DB initialization. +# For a personal app this is fine; async handles concurrent requests well. +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py index 9882258..97e142a 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -10,21 +10,27 @@ from app.api import auth, activities, routes, health, records, upload async def init_db(): - """Create tables then seed admin, with retries for slow DB startup.""" - for attempt in range(10): + """Create tables then seed admin, with retries for slow DB startup. + + Multiple uvicorn workers may race here on first start. We tolerate + duplicate table errors since they just mean another worker got there first. + """ + for attempt in range(15): try: - # Step 1: create all tables (separate connection so it commits cleanly) async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) break except Exception as e: - if attempt == 9: + msg = str(e).lower() + if "already exists" in msg or "duplicate" in msg or "pg_type_typname" in msg: + print("Tables already created by another worker - skipping") + break + if attempt == 14: raise - print(f"DB not ready yet (attempt {attempt + 1}/10): {e}") - await asyncio.sleep(3) + print(f"DB not ready yet (attempt {attempt + 1}/15): {e}") + await asyncio.sleep(2) - # Step 2: try to enable TimescaleDB hypertable (separate connection, - # failure here is non-fatal - falls back to plain Postgres) + # Try TimescaleDB hypertable (non-fatal) try: async with engine.begin() as conn: await conn.execute(text( @@ -34,24 +40,35 @@ async def init_db(): except Exception as e: print(f"TimescaleDB hypertable skipped: {e}") - # Step 3: seed admin user + # Seed admin user (only if password is configured) + if not settings.admin_password: + print("ADMIN_PASSWORD not set - skipping admin user seed") + return + from sqlalchemy import select from app.models.user import User from app.core.security import hash_password - async with AsyncSessionLocal() as db: - result = await db.execute( - select(User).where(User.username == settings.admin_username) - ) - if not result.scalar_one_or_none(): - admin = User( - username=settings.admin_username, - hashed_password=hash_password(settings.admin_password), - is_admin=True, + try: + async with AsyncSessionLocal() as db: + result = await db.execute( + select(User).where(User.username == settings.admin_username) ) - db.add(admin) - await db.commit() - print(f"Admin user '{settings.admin_username}' created") + if not result.scalar_one_or_none(): + admin = User( + username=settings.admin_username, + hashed_password=hash_password(settings.admin_password), + is_admin=True, + ) + db.add(admin) + await db.commit() + print(f"Admin user '{settings.admin_username}' created") + except Exception as e: + msg = str(e).lower() + if "duplicate" in msg or "unique" in msg: + print("Admin user already exists - skipping seed") + else: + raise @asynccontextmanager