Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .codacy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

exclude_paths:
- "assets/**/*"
- "data/**/*"
- "database/**/*"
- "models/**/*"
- "postman_collections/**/*"
- "schemas/**/*"
Expand Down
42 changes: 27 additions & 15 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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

Expand All @@ -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
Expand All @@ -52,4 +63,5 @@ ENV PYTHONUNBUFFERED=1

EXPOSE 9000

ENTRYPOINT ["./entrypoint.sh"]
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "9000"]
31 changes: 25 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ comment:
# https://docs.codecov.com/docs/ignoring-paths
ignore:
- "^assets/.*"
- "^data/.*"
- "^database/.*"
- "^models/.*"
- "^postman_collections/.*"
- "^schemas/.*"
Expand Down
4 changes: 3 additions & 1 deletion data/player_database.py → database/player_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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:
2 changes: 1 addition & 1 deletion routes/player_route.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion schemas/player_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
25 changes: 25 additions & 0 deletions scripts/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -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 "$@"
Binary file not shown.