Skip to content

Commit c59adf2

Browse files
committed
chore: add ci/cd
1 parent 860daf2 commit c59adf2

7 files changed

Lines changed: 229 additions & 39 deletions

File tree

.dockerignore

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
.git
2+
.github
3+
.mypy_cache
4+
.pytest_cache
5+
.ruff_cache
6+
.venv
7+
__pycache__
8+
*.py[cod]
9+
*.pyo
10+
*.pyd
11+
*.sqlite
12+
*.db
13+
.env
14+
.env.*
15+
!.env.example
16+
Dockerfile
17+
docker-compose*.yml
18+
docs
19+
tests
20+
README.md

.env.example

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
APP_NAME=Todo Modulith API
22
APP_ENV=production
33

4-
DATABASE_URL=postgresql+asyncpg://postgres:postgres@127.0.0.1:5432/todo_db
4+
POSTGRES_USER=postgres
5+
POSTGRES_PASSWORD=
6+
POSTGRES_DB=todo_db
7+
REDIS_PASSWORD=
8+
9+
DATABASE_URL=
510
DATABASE_POOL_SIZE=20
611
DATABASE_MAX_OVERFLOW=10
712
DATABASE_POOL_TIMEOUT=30
813
DATABASE_POOL_RECYCLE=3600
914

10-
REDIS_URL=redis://:password@127.0.0.1:6379/0
15+
REDIS_URL=
1116

12-
SECRET_KEY=your-super-secret-production-key-here
17+
SECRET_KEY=
1318

1419
MAX_REQUEST_SIZE_MB=5242880 #5mb
1520

.github/workflows/ci.yml

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
tags:
9+
- "v*.*.*"
10+
11+
permissions:
12+
contents: read
13+
packages: write
14+
15+
env:
16+
IMAGE_NAME: ghcr.io/${{ github.repository }}
17+
PYTHON_VERSION: "3.14"
18+
POETRY_VERSION: "2.4.1"
19+
20+
jobs:
21+
verify:
22+
name: Test and lint
23+
runs-on: ubuntu-latest
24+
25+
steps:
26+
- name: Check out repository
27+
uses: actions/checkout@v4
28+
29+
- name: Set up Python
30+
uses: actions/setup-python@v5
31+
with:
32+
python-version: ${{ env.PYTHON_VERSION }}
33+
34+
- name: Install Poetry
35+
run: pipx install poetry==${{ env.POETRY_VERSION }}
36+
37+
- name: Configure Poetry cache
38+
uses: actions/cache@v4
39+
with:
40+
path: ~/.cache/pypoetry
41+
key: poetry-${{ runner.os }}-${{ env.PYTHON_VERSION }}-${{ hashFiles('poetry.lock') }}
42+
restore-keys: |
43+
poetry-${{ runner.os }}-${{ env.PYTHON_VERSION }}-
44+
45+
- name: Install dependencies
46+
run: poetry install --with dev --no-interaction --no-ansi --no-root
47+
48+
- name: Run lint
49+
run: poetry run ruff check src tests scripts
50+
51+
- name: Run tests
52+
run: poetry run pytest -q
53+
54+
docker:
55+
name: Build and publish image
56+
runs-on: ubuntu-latest
57+
needs: verify
58+
59+
steps:
60+
- name: Check out repository
61+
uses: actions/checkout@v4
62+
63+
- name: Set up Docker Buildx
64+
uses: docker/setup-buildx-action@v3
65+
66+
- name: Log in to GHCR
67+
if: github.event_name == 'push'
68+
uses: docker/login-action@v3
69+
with:
70+
registry: ghcr.io
71+
username: ${{ github.actor }}
72+
password: ${{ secrets.GITHUB_TOKEN }}
73+
74+
- name: Extract Docker metadata
75+
id: meta
76+
uses: docker/metadata-action@v5
77+
with:
78+
images: ${{ env.IMAGE_NAME }}
79+
tags: |
80+
type=ref,event=branch
81+
type=ref,event=tag
82+
type=sha,prefix=sha-
83+
84+
- name: Build image
85+
uses: docker/build-push-action@v6
86+
with:
87+
context: .
88+
target: runtime
89+
push: ${{ github.event_name == 'push' }}
90+
tags: ${{ steps.meta.outputs.tags }}
91+
labels: ${{ steps.meta.outputs.labels }}
92+
cache-from: type=gha
93+
cache-to: type=gha,mode=max

Dockerfile

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,50 @@
1-
FROM python:3.11-slim
1+
FROM python:3.14-slim AS builder
2+
3+
ENV POETRY_VERSION=2.4.1 \
4+
POETRY_NO_INTERACTION=1 \
5+
POETRY_VIRTUALENVS_CREATE=1 \
6+
POETRY_VIRTUALENVS_IN_PROJECT=1 \
7+
PYTHONDONTWRITEBYTECODE=1 \
8+
PYTHONUNBUFFERED=1
29

310
WORKDIR /app
411

5-
# Install Poetry
6-
RUN pip install poetry
12+
RUN apt-get update \
13+
&& apt-get install --no-install-recommends -y build-essential \
14+
&& pip install --no-cache-dir "poetry==${POETRY_VERSION}" \
15+
&& rm -rf /var/lib/apt/lists/*
16+
17+
COPY pyproject.toml poetry.lock ./
18+
19+
RUN poetry install --only main --no-root --no-ansi
720

8-
# Copy dependency files
9-
COPY pyproject.toml poetry.lock* ./
1021

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

15-
# Copy source code
24+
ENV APP_ENV=production \
25+
PATH="/app/.venv/bin:${PATH}" \
26+
PYTHONDONTWRITEBYTECODE=1 \
27+
PYTHONUNBUFFERED=1
28+
29+
WORKDIR /app
30+
31+
RUN groupadd --system app \
32+
&& useradd --system --gid app --home-dir /app --shell /usr/sbin/nologin app
33+
34+
COPY --from=builder /app/.venv /app/.venv
35+
COPY alembic.ini ./
36+
COPY alembic ./alembic
37+
COPY scripts ./scripts
1638
COPY src ./src
1739

18-
# Expose port
40+
RUN chmod +x /app/scripts/start.sh \
41+
&& chown -R app:app /app
42+
43+
USER app
44+
1945
EXPOSE 8000
2046

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

25-
CMD ["./script/start.sh"]
50+
CMD ["/app/scripts/start.sh"]

docker-compose.yml

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,77 @@
11
services:
22
api:
3-
build: .
3+
build:
4+
context: .
5+
target: runtime
6+
image: fastapi-modulith:local
7+
restart: unless-stopped
48
ports:
5-
- "8000:8000"
9+
- "${APP_PORT:-8000}:8000"
610
env_file:
711
- .env
12+
environment:
13+
APP_ENV: production
14+
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}@db:5432/${POSTGRES_DB:-todo_db}
15+
REDIS_URL: redis://:${REDIS_PASSWORD:?Set REDIS_PASSWORD in .env}@redis:6379/0
816
depends_on:
9-
- db
10-
volumes:
11-
- ./src:/app/src
17+
db:
18+
condition: service_healthy
19+
redis:
20+
condition: service_healthy
21+
healthcheck:
22+
test:
23+
[
24+
"CMD",
25+
"python",
26+
"-c",
27+
"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3).read()",
28+
]
29+
interval: 30s
30+
timeout: 5s
31+
retries: 3
32+
start_period: 30s
1233

1334
db:
14-
image: postgres:15-alpine
35+
image: postgres:17-alpine
36+
restart: unless-stopped
1537
environment:
16-
POSTGRES_USER: postgres
17-
POSTGRES_PASSWORD: postgres
18-
POSTGRES_DB: todo_db
19-
ports:
20-
- "5432:5432"
38+
POSTGRES_USER: ${POSTGRES_USER:-postgres}
39+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
40+
POSTGRES_DB: ${POSTGRES_DB:-todo_db}
2141
volumes:
2242
- postgres_data:/var/lib/postgresql/data
43+
healthcheck:
44+
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
45+
interval: 10s
46+
timeout: 5s
47+
retries: 5
48+
49+
redis:
50+
image: redis:8-alpine
51+
restart: unless-stopped
52+
command:
53+
[
54+
"redis-server",
55+
"--appendonly",
56+
"yes",
57+
"--requirepass",
58+
"${REDIS_PASSWORD:?Set REDIS_PASSWORD in .env}",
59+
]
60+
volumes:
61+
- redis_data:/data
62+
healthcheck:
63+
test:
64+
[
65+
"CMD",
66+
"redis-cli",
67+
"-a",
68+
"${REDIS_PASSWORD:?Set REDIS_PASSWORD in .env}",
69+
"ping",
70+
]
71+
interval: 10s
72+
timeout: 5s
73+
retries: 5
2374

2475
volumes:
25-
postgres_data:
76+
postgres_data:
77+
redis_data:

scripts/start.sh

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
1-
#!/bin/bash
2-
# start.sh
1+
#!/usr/bin/env bash
2+
set -euo pipefail
33

44
echo "Running database migrations..."
5-
poetry run alembic upgrade head
6-
7-
if [ $? -ne 0 ]; then
8-
echo "Migration failed!"
9-
exit 1
10-
fi
5+
alembic upgrade head
116

127
echo "Starting FastAPI application..."
13-
exec poetry run uvicorn src.main:app --host 0.0.0.0 --port 8000 --workers 4
8+
exec uvicorn src.main:app --host 0.0.0.0 --port "${PORT:-8000}" --workers "${WEB_CONCURRENCY:-4}"

src/core/dependency/rate_limit.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from fastapi import HTTPException, Request, Response
1+
from fastapi import HTTPException, Request
22
from fastapi_limiter import FastAPILimiter
33

44
from src.core.config.setting import get_settings

0 commit comments

Comments
 (0)