From 5c83358a484c638ef769eb97b778b09f771687e1 Mon Sep 17 00:00:00 2001 From: Nano Taboada Date: Sat, 17 May 2025 21:34:54 -0300 Subject: [PATCH] feat(routes): add health check endpoint --- Dockerfile | 57 +++++++++++++++++++++++++----------------- main.py | 3 ++- routes/health_route.py | 15 +++++++++++ scripts/healthcheck.sh | 5 ++++ tests/test_main.py | 14 +++++++++++ 5 files changed, 70 insertions(+), 24 deletions(-) create mode 100644 routes/health_route.py create mode 100644 scripts/healthcheck.sh diff --git a/Dockerfile b/Dockerfile index 4b97e93..20975d3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,7 @@ # This stage builds the application and its dependencies. # ------------------------------------------------------------------------------ FROM python:3.13.3-slim-bookworm AS builder + WORKDIR /app # Install system build tools for packages with native extensions @@ -10,7 +11,7 @@ RUN apt-get update && \ apt-get install -y --no-install-recommends build-essential gcc libffi-dev libssl-dev && \ rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb -# Pre-build all dependencies into wheels for reproducibility and speed +# Build all dependencies into wheels for reproducibility and speed COPY --chown=root:root --chmod=644 requirements.txt . RUN pip wheel --no-cache-dir --wheel-dir=/app/wheelhouse -r requirements.txt @@ -19,49 +20,59 @@ RUN pip wheel --no-cache-dir --wheel-dir=/app/wheelhouse -r requirements.txt # This stage creates the final, minimal image to run the application. # ------------------------------------------------------------------------------ FROM python:3.13.3-slim-bookworm AS runtime + WORKDIR /app -# Metadata labels +# Install curl for health check +RUN apt-get update && apt-get install -y --no-install-recommends curl && \ + rm -rf /var/lib/apt/lists/* + +# Add metadata labels LABEL org.opencontainers.image.title="🧪 RESTful API with Python 3 and FastAPI" LABEL org.opencontainers.image.description="Proof of Concept for a RESTful API made with Python 3 and FastAPI" LABEL org.opencontainers.image.licenses="MIT" LABEL org.opencontainers.image.source="https://github.com/nanotaboada/python-samples-fastapi-restful" -# Copy prebuilt wheels and install dependencies -COPY --chown=root:root --chmod=644 requirements.txt . -COPY --from=builder --chown=root:root --chmod=755 /app/wheelhouse /app/wheelhouse +# Copy metadata docs for container registries (e.g.: GitHub Container Registry) +COPY README.md ./ +COPY assets ./assets + +# Copy pre-built wheels from builder +COPY --from=builder /app/wheelhouse /app/wheelhouse + +# Install dependencies +COPY requirements.txt . RUN pip install --no-cache-dir --no-index --find-links /app/wheelhouse -r requirements.txt && \ rm -rf /app/wheelhouse -# Copy application code (read-only) -COPY --chown=root:root --chmod=644 main.py ./ -COPY --chown=root:root --chmod=755 database ./database -COPY --chown=root:root --chmod=755 models ./models -COPY --chown=root:root --chmod=755 routes ./routes -COPY --chown=root:root --chmod=755 schemas ./schemas -COPY --chown=root:root --chmod=755 services ./services +# Copy application source code +COPY main.py ./ +COPY database ./database +COPY models ./models +COPY routes ./routes +COPY schemas ./schemas +COPY services ./services -# Copy metadata for GHCR (read-only) -COPY --chown=root:root --chmod=644 README.md ./ -COPY --chown=root:root --chmod=755 assets ./assets +# Copy entrypoint script and image-bundled, pre-seeded SQLite database +COPY --chmod=755 scripts/entrypoint.sh ./entrypoint.sh +COPY --chmod=755 scripts/healthcheck.sh ./healthcheck.sh +COPY --chmod=755 storage ./docker-compose -# Copy entrypoint sctipt and SQLite database -COPY --chown=root:root --chmod=755 scripts/entrypoint.sh ./entrypoint.sh -COPY --chown=root:root --chmod=755 storage ./docker-compose - -# Create non-root user and make volume mount point writable +# Add non-root user and make volume mount point writable RUN groupadd --system fastapi && \ adduser --system --ingroup fastapi --disabled-password --gecos '' fastapi && \ mkdir -p /storage && \ chown fastapi:fastapi /storage +ENV PYTHONUNBUFFERED=1 + # Drop privileges USER fastapi -# Logging output immediately -ENV PYTHONUNBUFFERED=1 - EXPOSE 9000 +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD ["./healthcheck.sh"] + ENTRYPOINT ["./entrypoint.sh"] CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "9000"] diff --git a/main.py b/main.py index 881c878..85fbc3a 100644 --- a/main.py +++ b/main.py @@ -6,7 +6,7 @@ import logging from typing import AsyncIterator from fastapi import FastAPI -from routes import player_route +from routes import player_route, health_route # https://github.com/encode/uvicorn/issues/562 UVICORN_LOGGER = "uvicorn.error" @@ -26,3 +26,4 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]: version="1.0.0",) app.include_router(player_route.api_router) +app.include_router(health_route.api_router) diff --git a/routes/health_route.py b/routes/health_route.py new file mode 100644 index 0000000..c206abf --- /dev/null +++ b/routes/health_route.py @@ -0,0 +1,15 @@ +# ------------------------------------------------------------------------------ +# Route +# ------------------------------------------------------------------------------ + +from fastapi import APIRouter + + +api_router = APIRouter() + +@api_router.get("/health", tags=["Health"]) +async def health_check(): + """ + Simple health check endpoint. Returns a JSON response with a single key "status" and value "ok". + """ + return {"status": "ok"} diff --git a/scripts/healthcheck.sh b/scripts/healthcheck.sh new file mode 100644 index 0000000..9f5a04b --- /dev/null +++ b/scripts/healthcheck.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -e + +# Simple health check using curl +curl --fail http://localhost:9000/health diff --git a/tests/test_main.py b/tests/test_main.py index f705ee7..bf9a600 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -7,6 +7,20 @@ PATH = "/players/" +# GET /health/ ----------------------------------------------------------------- + +def test_given_get_when_request_path_is_health_then_response_status_code_should_be_200_ok(client): + """ + Given GET /health/ + when request + then response Status Code should be 200 OK + """ + # Act + response = client.get("/health/") + # Assert + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + # GET /players/ ----------------------------------------------------------------