From f53ef813069af47b4cde649dbb08b233bd8ec0e6 Mon Sep 17 00:00:00 2001 From: Nikolay Tatarinov Date: Fri, 20 Feb 2026 20:31:43 +0300 Subject: [PATCH] chore: update Docker configuration and improve health check functionality - Added Docker health check endpoint to the FastAPI application, returning a 200 status when the app is running. - Updated Dockerfile to include curl for health checks and modified entrypoint script to exit on errors. - Enhanced .dockerignore and .gitignore files to exclude coverage and test artifacts. - Updated docker-compose.prod.yml to specify version. - Added pytest-cov as a development dependency to improve test coverage reporting. --- .coverage | Bin 53248 -> 53248 bytes .dockerignore | 1 - .gitignore | 6 ++++++ Dockerfile | 13 +++++++++---- docker-compose.prod.yml | 2 ++ duty_teller/api/app.py | 8 ++++++++ entrypoint.sh | 2 +- requirements-dev.txt | 1 + tests/test_app.py | 7 +++++++ 9 files changed, 34 insertions(+), 6 deletions(-) diff --git a/.coverage b/.coverage index f6d87fe7d552c1a751ab74ff8a0e7e730c034be5..179c1d7507dbc7eab6c4326228e9a52d08a03db6 100644 GIT binary patch delta 1783 zcmZwHTTB#J7zgmPoWa?1&dfP`0atEI z*q!u}Py;lbq%qM{($vPrq}A|J1q1C%L0XONLtm(FYzTd+7;7Sx%l4?SJF|8lfAf9c z`R2?&8ytbb5%_6KT=&4W!(W!T@&NmZeZnf4nck+K(K?zYKajtY-jhamhBHo=2!Li&U?;USd-}G)XcXqY!?C7c8 zEC3}vqUx#23{sKC%^J0-%4AZK$_+-%;aE^HTvl%JQ9EDFu-yWIKSJi_jt+iMv6&Yd)^(dYT(dzjR>DOR1c4~IYfZR_=xE?MVI56#%164rj2;>18?|IR z+>+}*bfooov!5^P6m>@jJrXY1pxb}ULrG~o-#_w!bJP`&3FiqAoQQ5%ew{t(WAeV_t$C1_-)d(p_(!%MRESJ4+@>KXa2k^bWI7mDbCb=w|s(^f4+&amMS$ zqw+h_g1j9s;IEB#dl0(?9H*FBv-K(X4MEA&G`>jkf#lfcm>}C?28|y`!R3O2 zG=pB`xIw~If?|s>k8wj1E)|q`4V`Z=AqVtpLTd?blu!Ze(d&AEO@**m z`=pWm&j@XPX+AuuX^;=fXfF(C#C^~lBLlCpW{9hTp>&weIBr3|Q@|^sed`rG5Uz0EM)V9d#H-mP=vKksJ zkNcuf%^KQjm{Tii`r#(yK7A(WudGf#9{<03n4%# zdtzcVmZkAD(Ke}4pKQfM<)xaSs}J6YHI2qwA`r3G7b1y`C9QOwtzh{CH56N$qqBS^s{tEYL)WoB0Wc+qE50x zE|Ozvv2je$J01A@NFCnG(B5v|ehvRmi zxCa5XbPH<_G-K_&Cy#oHdBC0#So4^vQp5wfX#op$7jlTxAlAm333V6TB{0tep8O1b z#fx9{n(5=)IWN;GDc+sbotL4)leM#6uHwj0DfWrlqSr*5>^wkpGQ~bS_1ZYh*TGI- z5pA?`Xwbn~pOsWwAQ;xMRi8**W}XOBaJN?BC#22^$m3QJeNE|{Wks4oLW4k90nY=@e$&ZmD z_O;B|6njEC%N)`#xEGfhSBz(j%~FUiNd@AM;*`-QHsc%kUHS&?B)`)#ZvM*#@nP7f zYv!i+cLb4Is>A<#|HyXIj8uCjUprzUjY!Q+WGgjAfy`1=`arFZ4?oM&n$Z6C6b z!$>XD%P65;SrT_lAIg@v_;WyLaLx=El15+>~uEE za(Y^Me(v-RmW2Y`=pCY%UPYSE4QZ$1KJSpTu41FZE>^>czGzoi%buAYVTUb*$*j5@bEZB_mQ1Y9Z~ diff --git a/.dockerignore b/.dockerignore index ca3c9c9..0d711f5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,7 +9,6 @@ __pycache__/ .cursor/ *.plan.md data/ -.curosr/ # Tests and dev artifacts (not needed in image) tests/ .pytest_cache/ diff --git a/.gitignore b/.gitignore index 21f47da..6c3bfbf 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,9 @@ venv/ data/ *.db .cursor/ +# Test and coverage artifacts +.coverage +htmlcov/ +.pytest_cache/ +*.cover +*.plan.md diff --git a/Dockerfile b/Dockerfile index 31e2155..dfa32cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,16 +4,17 @@ # --- Stage 1: builder (dependencies only) --- FROM python:3.12-slim AS builder WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt +COPY pyproject.toml ./ +COPY duty_teller/ ./duty_teller/ +RUN pip install --no-cache-dir . # --- Stage 2: runtime (minimal final image) --- FROM python:3.12-slim WORKDIR /app -# Install gosu (drop privileges in entrypoint) -RUN apt-get update && apt-get install -y --no-install-recommends gosu \ +# Install gosu (drop privileges in entrypoint) and curl (for HEALTHCHECK) +RUN apt-get update && apt-get install -y --no-install-recommends gosu curl \ && rm -rf /var/lib/apt/lists/* # Copy installed packages and console scripts from builder (no requirements.txt, no pip layer) @@ -23,6 +24,7 @@ COPY --from=builder /usr/local/bin /usr/local/bin # Application code (duty_teller package + entrypoint, migrations, webapp) ENV PYTHONPATH=/app COPY main.py pyproject.toml entrypoint.sh ./ +RUN chmod +x entrypoint.sh COPY duty_teller/ ./duty_teller/ COPY alembic/ ./alembic/ COPY webapp/ ./webapp/ @@ -34,3 +36,6 @@ RUN adduser --disabled-password --gecos "" botuser \ # Entrypoint runs as root: fix /app/data ownership (for volume mount), run migrations, then exec as botuser ENTRYPOINT ["/bin/sh", "./entrypoint.sh"] CMD ["python", "main.py"] + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD curl -f http://localhost:8080/health || exit 1 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 4fc86b8..8eda172 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,3 +1,5 @@ +version: "3.8" + services: bot: build: diff --git a/duty_teller/api/app.py b/duty_teller/api/app.py index f01a69c..6c0a13a 100644 --- a/duty_teller/api/app.py +++ b/duty_teller/api/app.py @@ -35,6 +35,14 @@ def _is_valid_calendar_token(token: str) -> bool: app = FastAPI(title="Duty Teller API") + + +@app.get("/health", summary="Health check") +def health() -> dict: + """Return 200 when the app is up. Used by Docker HEALTHCHECK.""" + return {"status": "ok"} + + app.add_middleware( CORSMiddleware, allow_origins=config.CORS_ORIGINS, diff --git a/entrypoint.sh b/entrypoint.sh index 6558967..bcfef04 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,5 +1,5 @@ #!/bin/sh -set -e +set -eu # Ensure data dir exists and is writable by botuser (volume may be root-owned) mkdir -p /app/data chown botuser:botuser /app/data diff --git a/requirements-dev.txt b/requirements-dev.txt index a9bb33e..2ddce6f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,5 @@ -r requirements.txt pytest>=8.0,<9.0 pytest-asyncio>=1.0,<2.0 +pytest-cov>=6.0,<7.0 httpx>=0.27,<1.0 diff --git a/tests/test_app.py b/tests/test_app.py index 0da8233..14cc645 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -16,6 +16,13 @@ def client(): return TestClient(app) +def test_health(client): + """Health endpoint returns 200 and status ok for Docker HEALTHCHECK.""" + r = client.get("/health") + assert r.status_code == 200 + assert r.json() == {"status": "ok"} + + def test_duties_invalid_date_format(client): r = client.get("/api/duties", params={"from": "2025-01-01", "to": "invalid"}) assert r.status_code == 400