Skip to content

Commit ec97c95

Browse files
nanotaboadaclaude
andcommitted
feat(server): use Gunicorn + UvicornWorker with on_starting migration hook (#2)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a729939 commit ec97c95

File tree

6 files changed

+52
-7
lines changed

6 files changed

+52
-7
lines changed

CHANGELOG.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,12 @@ This project uses famous football coaches as release codenames, following an A-Z
5050
11 Starting XI players, `003` seeds 15 Substitute players (all with
5151
deterministic UUID v5 values); `alembic upgrade head` applied by
5252
`entrypoint.sh` (Docker) or manually for local development (#2)
53-
- `alembic==1.18.4`, `asyncpg==0.31.0` added to dependencies (#2)
53+
- `alembic==1.18.4`, `asyncpg==0.31.0`, `gunicorn==25.3.0` added to dependencies (#2)
54+
- `gunicorn.conf.py`: Gunicorn configuration — binds to `0.0.0.0:9000`, uses
55+
`UvicornWorker`, derives worker count from `WEB_CONCURRENCY` env var; the
56+
`on_starting` hook runs `alembic upgrade head` once in the master process
57+
before any workers are forked, replacing the entrypoint-driven migration
58+
pattern (#2)
5459
- `tests/test_migrations.py`: integration tests for migration downgrade paths —
5560
verifies each step removes only its seeded rows and restores correctly (#2)
5661
- `tests/conftest.py`: session-scoped `apply_migrations` fixture runs
@@ -78,8 +83,10 @@ This project uses famous football coaches as release codenames, following an A-Z
7883
- `Dockerfile`: removed `COPY storage/ ./hold/` and its associated comment;
7984
added `COPY alembic.ini` and `COPY alembic/` (#2)
8085
- `scripts/entrypoint.sh`: checks for an existing database file in the Docker
81-
volume; runs `alembic upgrade head` only on first start; adds structured
82-
`log()` helper with timestamps, emojis, and API/Swagger UI addresses (#2)
86+
volume (informational logging only); adds structured `log()` helper with
87+
timestamps and API/Swagger UI addresses; migrations delegated to Gunicorn
88+
`on_starting` hook (#2)
89+
- `Dockerfile`: replaced `CMD uvicorn` with `CMD gunicorn -c gunicorn.conf.py` (#2)
8390
- `compose.yaml`: replaced `STORAGE_PATH` with `DATABASE_URL` pointing to the
8491
SQLite volume path (#2)
8592
- `.gitignore`: added `*.db`; `storage/players-sqlite3.db` removed from git

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ RUN pip install --no-cache-dir --no-index --find-links /app/wheelhouse /app/whee
5151

5252
# Copy application source code
5353
COPY main.py ./
54+
COPY gunicorn.conf.py ./
5455
COPY alembic.ini ./
5556
COPY alembic/ ./alembic/
5657
COPY databases/ ./databases/
@@ -80,4 +81,4 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
8081
CMD ["./healthcheck.sh"]
8182

8283
ENTRYPOINT ["./entrypoint.sh"]
83-
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "9000"]
84+
CMD ["gunicorn", "-c", "gunicorn.conf.py", "main:app"]

gunicorn.conf.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""
2+
Gunicorn configuration for production deployment.
3+
4+
Uses UvicornWorker to run the FastAPI ASGI app. The on_starting hook runs
5+
Alembic migrations once in the master process before any workers are forked,
6+
ensuring a single, race-free initialization step.
7+
"""
8+
9+
import multiprocessing
10+
import os
11+
from pathlib import Path
12+
13+
from alembic import command
14+
from alembic.config import Config
15+
16+
bind: str = "0.0.0.0:9000"
17+
workers: int = int(os.getenv("WEB_CONCURRENCY", multiprocessing.cpu_count() * 2 + 1))
18+
worker_class: str = "uvicorn.workers.UvicornWorker"
19+
20+
21+
def on_starting(server) -> None:
22+
"""Apply Alembic migrations once before workers are spawned."""
23+
alembic_cfg = Config(str(Path(__file__).resolve().parent / "alembic.ini"))
24+
command.upgrade(alembic_cfg, "head")

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ dependencies = [
1212
"aiocache==0.12.3",
1313
"alembic==1.18.4",
1414
"asyncpg==0.31.0",
15+
"gunicorn>=25.3.0",
1516
]
1617

1718
[dependency-groups]

scripts/entrypoint.sh

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@ mkdir -p "$(dirname "$STORAGE_PATH")"
1515

1616
if [ ! -f "$STORAGE_PATH" ]; then
1717
log "⚠️ No existing database file found in volume."
18-
log "🗄️ Applying Alembic migrations to initialize the database..."
19-
alembic upgrade head
20-
log "✔ Migrations applied."
18+
log "🗄️ Gunicorn will apply Alembic migrations on first start."
2119
else
2220
log "✔ Existing database file found at $STORAGE_PATH."
2321
fi

uv.lock

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)