diff --git a/.codacy.yml b/.codacy.yml index 4875560..2c9ccd2 100644 --- a/.codacy.yml +++ b/.codacy.yml @@ -2,7 +2,7 @@ exclude_paths: - "assets/**/*" - - "data/**/*" + - "database/**/*" - "models/**/*" - "postman_collections/**/*" - "schemas/**/*" diff --git a/Dockerfile b/Dockerfile index c1ed918..19c6f8a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,7 @@ -# - Stage 1: Builder ----------------------------------------------------------- - +# ------------------------------------------------------------------------------ +# Stage 1: Builder +# This stage builds the application and its dependencies. +# ------------------------------------------------------------------------------ FROM python:3.13.3-slim-bookworm AS builder WORKDIR /app @@ -12,8 +14,10 @@ RUN apt-get update && \ COPY --chown=root:root --chmod=644 requirements.txt . RUN pip wheel --no-cache-dir --wheel-dir=/app/wheelhouse -r requirements.txt -# - Stage 2: Runtime ----------------------------------------------------------- - +# ------------------------------------------------------------------------------ +# Stage 2: Runtime +# This stage creates the final, minimal image to run the application. +# ------------------------------------------------------------------------------ FROM python:3.13.3-slim-bookworm AS runtime WORKDIR /app @@ -24,25 +28,32 @@ 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 --from=builder --chown=root:root --chmod=755 /app/wheelhouse /app/wheelhouse COPY --chown=root:root --chmod=644 requirements.txt . +COPY --from=builder --chown=root:root --chmod=755 /app/wheelhouse /app/wheelhouse 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 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 --chown=root:root --chmod=755 data ./data +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 metadata for GHCR (read-only) -COPY --chown=root:root --chmod=644 README.md ./ -COPY --chown=root:root --chmod=755 assets ./assets +COPY --chown=root:root --chmod=644 README.md ./ +COPY --chown=root:root --chmod=755 assets ./assets + +# Copy entrypoint sctipt and SQLite database +COPY --chown=root:root --chmod=755 scripts/entrypoint.sh ./entrypoint.sh +COPY --chown=root:root --chmod=755 sqlite3-db ./docker-compose -# Create a non-root user for running the app -RUN adduser --system --disabled-password --gecos '' fastapi +# Create non-root user and make volume mount point writable +RUN groupadd --system fastapi && \ + adduser --system --ingroup fastapi --disabled-password --gecos '' fastapi && \ + mkdir -p /sqlite3-db && \ + chown fastapi:fastapi /sqlite3-db # Drop privileges USER fastapi @@ -52,4 +63,5 @@ ENV PYTHONUNBUFFERED=1 EXPOSE 9000 +ENTRYPOINT ["./entrypoint.sh"] CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "9000"] diff --git a/README.md b/README.md index c5ab57e..7bd31b2 100644 --- a/README.md +++ b/README.md @@ -41,22 +41,41 @@ http://localhost:9000/docs ![API Documentation](assets/images/swagger.png) -## Docker +## Container -This project includes a multi-stage `Dockerfile` for local development and production builds. +### Docker Compose -### Build the image +This setup uses [Docker Compose](https://docs.docker.com/compose/) to build and run the app and manage a persistent SQLite database stored in a Docker volume. + +#### Build the image + +```bash +docker compose build +``` + +#### Start the app ```bash -docker build -t python-samples-fastapi-restful . +docker compose up ``` -### Run the container +> On first run, the container copies a pre-seeded SQLite database into a persistent volume +> On subsequent runs, that volume is reused and the data is preserved + +#### Stop the app ```bash -docker run -p 9000:9000 python-samples-fastapi-restful:latest +docker compose down ``` +#### Optional: database reset + +```bash +docker compose down -v +``` + +> This removes the volume and will reinitialize the database from the built-in seed file the next time you `up`. + ## Credits The solution has been coded using [Visual Studio Code](https://code.visualstudio.com/) with the official [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python) extension. diff --git a/codecov.yml b/codecov.yml index cdb57d2..e58090c 100644 --- a/codecov.yml +++ b/codecov.yml @@ -40,7 +40,7 @@ comment: # https://docs.codecov.com/docs/ignoring-paths ignore: - "^assets/.*" - - "^data/.*" + - "^database/.*" - "^models/.*" - "^postman_collections/.*" - "^schemas/.*" diff --git a/data/player_database.py b/database/player_database.py similarity index 87% rename from data/player_database.py rename to database/player_database.py index f8f9051..dbe96b5 100644 --- a/data/player_database.py +++ b/database/player_database.py @@ -3,11 +3,13 @@ # ------------------------------------------------------------------------------ import logging +import os from typing import AsyncGenerator from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.orm import sessionmaker, declarative_base -DATABASE_URL = "sqlite+aiosqlite:///./data/players-sqlite3.db" +database_file_path = os.getenv("DATABASE_FILE_PATH", "./sqlite3-db/players-sqlite3.db") +DATABASE_URL = f"sqlite+aiosqlite:///{database_file_path}" logger = logging.getLogger("uvicorn") logging.getLogger("sqlalchemy.engine.Engine").handlers = logger.handlers diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..71f4d58 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +services: + api: + image: python-samples-fastapi-restful + container_name: fastapi-app + build: + context: . + dockerfile: Dockerfile + ports: + - "9000:9000" + volumes: + - sqlite3-db:/sqlite3-db/ + environment: + - PYTHONUNBUFFERED=1 + - DATABASE_FILE_PATH=/sqlite3-db/players-sqlite3.db + restart: unless-stopped + +volumes: + sqlite3-db: diff --git a/routes/player_route.py b/routes/player_route.py index 3afd562..e570bba 100644 --- a/routes/player_route.py +++ b/routes/player_route.py @@ -7,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from aiocache import SimpleMemoryCache -from data.player_database import generate_async_session +from database.player_database import generate_async_session from models.player_model import PlayerModel from services import player_service diff --git a/schemas/player_schema.py b/schemas/player_schema.py index 3c34a03..857997c 100644 --- a/schemas/player_schema.py +++ b/schemas/player_schema.py @@ -3,7 +3,7 @@ # ------------------------------------------------------------------------------ from sqlalchemy import Column, String, Integer, Boolean -from data.player_database import Base +from database.player_database import Base class Player(Base): diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh new file mode 100644 index 0000000..642f010 --- /dev/null +++ b/scripts/entrypoint.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -e + +IMAGE_DATABASE_FILE_PATH="/app/docker-compose/players-sqlite3.db" +VOLUME_DATABASE_FILE_PATH="/sqlite3-db/players-sqlite3.db" + +echo "✔ Starting container..." + +if [ ! -f "$VOLUME_DATABASE_FILE_PATH" ]; then + echo "⚠️ No existing database file found in volume." + if [ -f "$IMAGE_DATABASE_FILE_PATH" ]; then + echo "Copying database file to writable volume..." + cp "$IMAGE_DATABASE_FILE_PATH" "$VOLUME_DATABASE_FILE_PATH" + echo "✔ Database initialized at $VOLUME_DATABASE_FILE_PATH" + else + echo "⚠️ Database file missing at $IMAGE_DATABASE_FILE_PATH" + exit 1 + fi +else + echo "✔ Existing database file found. Skipping seed copy." +fi + +echo "✔ Ready!" +echo "🚀 Launching app..." +exec "$@" diff --git a/data/players-sqlite3.db b/sqlite3-db/players-sqlite3.db similarity index 99% rename from data/players-sqlite3.db rename to sqlite3-db/players-sqlite3.db index 8d6a986..07bb620 100644 Binary files a/data/players-sqlite3.db and b/sqlite3-db/players-sqlite3.db differ