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
20 changes: 20 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.git
.github
.mypy_cache
.pytest_cache
.ruff_cache
.venv
__pycache__
*.py[cod]
*.pyo
*.pyd
*.sqlite
*.db
.env
.env.*
!.env.example
Dockerfile
docker-compose*.yml
docs
tests
README.md
41 changes: 37 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,14 +1,47 @@
APP_NAME=Todo Modulith API
APP_ENV=production

DATABASE_URL=postgresql+asyncpg://postgres:postgres@127.0.0.1:5432/todo_db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=
POSTGRES_DB=todo_db
REDIS_PASSWORD=

REDIS_URL=redis://:password@127.0.0.1:6379/0
DATABASE_URL=
DATABASE_POOL_SIZE=20
DATABASE_MAX_OVERFLOW=10
DATABASE_POOL_TIMEOUT=30
DATABASE_POOL_RECYCLE=3600

SECRET_KEY=your-super-secret-production-key-here
REDIS_URL=

SECRET_KEY=

MAX_REQUEST_SIZE_MB=5242880 #5mb

ALGORITHM=HS256
JWT_ISSUER=todo-modulith-api
JWT_AUDIENCE=todo-modulith-client
ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_MINUTES=10080

RATE_LIMIT="100/minute"
RATE_LIMIT="100/minute"

CORS_ALLOW_ORIGINS=http://localhost:3000
CORS_ALLOW_METHODS=*
CORS_ALLOW_HEADERS=*

SECURITY_CONTENT_SECURITY_POLICY=default-src 'self'; frame-ancestors 'none'

IDEMPOTENCY_TTL_SECONDS=86400

ACCOUNT_LOCKOUT_MAX_ATTEMPTS=5
ACCOUNT_LOCKOUT_WINDOW_MINUTES=15
ACCOUNT_LOCKOUT_DURATION_MINUTES=15

LOG_FORMAT=json

SEED_ADMIN_EMAIL=
SEED_ADMIN_PASSWORD=
SEED_ADMIN_USERNAME=admin
SEED_ADMIN_FULLNAME=System Administrator
SEED_DEVELOPMENT_USERS_PASSWORD=
93 changes: 93 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: CI

on:
pull_request:
push:
branches:
- main
tags:
- "v*.*.*"

permissions:
contents: read
packages: write

env:
IMAGE_NAME: ghcr.io/${{ github.repository }}
PYTHON_VERSION: "3.14"
POETRY_VERSION: "2.4.1"

jobs:
verify:
name: Test and lint
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Install Poetry
run: pipx install poetry==${{ env.POETRY_VERSION }}

- name: Configure Poetry cache
uses: actions/cache@v4
with:
path: ~/.cache/pypoetry
key: poetry-${{ runner.os }}-${{ env.PYTHON_VERSION }}-${{ hashFiles('poetry.lock') }}
restore-keys: |
poetry-${{ runner.os }}-${{ env.PYTHON_VERSION }}-

- name: Install dependencies
run: poetry install --with dev --no-interaction --no-ansi --no-root

- name: Run lint
run: poetry run ruff check src tests scripts

- name: Run tests
run: poetry run pytest -q

docker:
name: Build and publish image
runs-on: ubuntu-latest
needs: verify

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
if: github.event_name == 'push'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=sha-

- name: Build image
uses: docker/build-push-action@v6
with:
context: .
target: runtime
push: ${{ github.event_name == 'push' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
53 changes: 39 additions & 14 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,25 +1,50 @@
FROM python:3.11-slim
FROM python:3.14-slim AS builder

ENV POETRY_VERSION=2.4.1 \
POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_CREATE=1 \
POETRY_VIRTUALENVS_IN_PROJECT=1 \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1

WORKDIR /app

# Install Poetry
RUN pip install poetry
RUN apt-get update \
&& apt-get install --no-install-recommends -y build-essential \
&& pip install --no-cache-dir "poetry==${POETRY_VERSION}" \
&& rm -rf /var/lib/apt/lists/*

COPY pyproject.toml poetry.lock ./

RUN poetry install --only main --no-root --no-ansi

# Copy dependency files
COPY pyproject.toml poetry.lock* ./

# Install dependencies without dev tools
RUN poetry config virtualenvs.create false \
&& poetry install --no-interaction --no-ansi --no-root
FROM python:3.14-slim AS runtime

# Copy source code
ENV APP_ENV=production \
PATH="/app/.venv/bin:${PATH}" \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1

WORKDIR /app

RUN groupadd --system app \
&& useradd --system --gid app --home-dir /app --shell /usr/sbin/nologin app

COPY --from=builder /app/.venv /app/.venv
COPY alembic.ini ./
COPY alembic ./alembic
COPY scripts ./scripts
COPY src ./src

# Expose port
RUN chmod +x /app/scripts/start.sh \
&& chown -R app:app /app

USER app

EXPOSE 8000

# Run application
COPY start.sh ./script/start.sh
RUN chmod +x start.sh
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3).read()" || exit 1

CMD ["./script/start.sh"]
CMD ["/app/scripts/start.sh"]
18 changes: 16 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ COMPOSE_FILE := docker-compose.yml

.DEFAULT_GOAL := help

.PHONY: help install run test lint import-check check migrate downgrade revision db-up db-down db-logs clean
.PHONY: help install run test lint import-check security-scan check migrate seed downgrade revision db-up db-down db-logs clean

help:
@echo "[make:help] Available commands:"
Expand All @@ -19,8 +19,10 @@ help:
@echo " [make:test] Run pytest"
@echo " [make:lint] Run Ruff checks"
@echo " [make:import-check] Verify src.main imports"
@echo " [make:security-scan] Run dependency vulnerability scan with pip-audit"
@echo " [make:check] Run tests, lint, and import check"
@echo " [make:migrate] Apply Alembic migrations"
@echo " [make:seed] Seed baseline database records"
@echo " [make:downgrade] Roll back one Alembic migration"
@echo " [make:revision] Create an Alembic migration: make revision name=\"describe change\""
@echo " [make:db-up] Start Docker Compose services"
Expand All @@ -42,19 +44,31 @@ test:

lint:
@echo "[make:lint] Running Ruff checks"
@$(RUFF) check src tests
@$(RUFF) check src tests scripts

import-check:
@echo "[make:import-check] Verifying src.main imports"
@PYTHONDONTWRITEBYTECODE=1 $(PYTHON) -c "import src.main; print('import ok')"

security-scan:
@echo "[make:security-scan] Running dependency vulnerability scan"
@if ! command -v pip-audit >/dev/null 2>&1; then \
echo "[make:security-scan] pip-audit is not installed. Install it with: pip install pip-audit"; \
exit 1; \
fi
@pip-audit

check: test lint import-check
@echo "[make:check] All checks completed"

migrate:
@echo "[make:migrate] Applying Alembic migrations"
@$(ALEMBIC) upgrade head

seed:
@echo "[make:seed] Running database seeders"
@$(PYTHON) scripts/seed.py

downgrade:
@echo "[make:downgrade] Rolling back one Alembic migration"
@$(ALEMBIC) downgrade -1
Expand Down
Loading
Loading