Docker Compose deploy ready

This commit is contained in:
2025-08-29 16:49:46 +04:00
parent 75fa993fdd
commit d8644cf921
9 changed files with 165 additions and 24 deletions

23
backend/.env.example Normal file
View File

@@ -0,0 +1,23 @@
DATABASE_DRIVER=mysql+aiomysql
DATABASE_HOST=mysql
DATABASE_PORT=3306
DATABASE_NAME=app
DATABASE_USERNAME=root
DATABASE_PASSWORD=supersecret
SMTP_HOST=smtp.example.com
SMTP_PORT=2525
SMTP_USERNAME=email@example.com
SMTP_FROM=email@example.com
SMTP_PASSWORD=password
# OTP_CODE_EXPIRED_TIME = 5
REDIS_HOST=redis
REDIS_PORT=6379
# REDIS_USERNAME=
# REDIS_PASSWORD=
# REDIS_DB=0
# JWT_ALGORITHM=HS256
JWT_SECRET_KEY=SECRET_KEY

View File

@@ -0,0 +1,53 @@
# Базовый образ для сборки
FROM python:3.12.3-slim-bookworm AS builder
# Установка системных зависимостей для сборки
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Создание виртуального окружения
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Копирование и установка зависимостей
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
# Финальный образ
FROM python:3.12.3-slim-bookworm
# Установка только необходимых системных пакетов
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*
# Копирование виртуального окружения из builder
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Создание пользователя для безопасности
RUN groupadd -r fastapi && useradd -r -g fastapi fastapi
# Создание рабочей директории
WORKDIR /app
# Копирование приложения
COPY --chown=fastapi:fastapi . .
# Переключение на непривилегированного пользователя
USER fastapi
ENTRYPOINT ["/app/entrypoint.sh"]
# Экспорт порта
EXPOSE 8000
# Переменные окружения
ENV PYTHONPATH=/app \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
# Команда запуска
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -11,7 +11,7 @@ def get_password_hash(password: str) -> str:
jwt_config = AuthXConfig( jwt_config = AuthXConfig(
JWT_ALGORITHM=app.config.jwt_algorithm, JWT_ALGORITHM=app.config.jwt_algorithm,
JWT_SECRET_KEY=app.config.swt_secret_key, JWT_SECRET_KEY=app.config.jwt_secret_key,
JWT_TOKEN_LOCATION=app.config.jwt_token_location, JWT_TOKEN_LOCATION=app.config.jwt_token_location,
) )

View File

@@ -1,19 +1,34 @@
database_url = "sqlite+aiosqlite:///data/database.sqlite" import os
smtp_host = "smtp.beget.com" database_driver = os.getenv("DATABASE_DRIVER")
smtp_port = 2525 database_host = os.getenv("DATABASE_HOST")
smtp_username = "university@nspotapov.ru" database_port = int(os.getenv("DATABASE_PORT"))
smtp_from = "university@nspotapov.ru" database_name = os.getenv("DATABASE_NAME")
smtp_password = "V%P2WUe2wnwL" database_username = os.getenv("DATABASE_USERNAME")
database_password = os.getenv("DATABASE_PASSWORD")
otp_code_expired_time = 5 # minutes database_url = ''.join(
[
database_driver, "://",
database_username, ":", database_password, ("@" if database_username or database_password else ""),
database_host, (":" if str(database_port) else ""), str(database_port), "/", database_name
]
)
redis_host = "127.0.0.1" smtp_host = os.getenv("SMTP_HOST")
redis_port = 6379 smtp_port = os.getenv("SMTP_PORT")
redis_username = None smtp_username = os.getenv("SMTP_USERNAME")
redis_password = None smtp_from = os.getenv("SMTP_FROM")
redis_db = 0 smtp_password = os.getenv("SMTP_PASSWORD")
jwt_algorithm = "HS256" otp_code_expired_time = int(os.getenv("OTP_CODE_EXPIRED_TIME", "5")) # minutes
swt_secret_key = "SECRET_KEY"
redis_host = os.getenv("REDIS_HOST")
redis_port = int(os.getenv("REDIS_PORT"))
redis_username = os.getenv("REDIS_USERNAME")
redis_password = os.getenv("REDIS_PASSWORD")
redis_db = int(os.getenv("REDIS_DB", "0"))
jwt_algorithm = os.getenv("JWT_ALGORITHM", "HS256")
jwt_secret_key = os.getenv("JWT_SECRET_KEY")
jwt_token_location = ["headers", "cookies", "query"] jwt_token_location = ["headers", "cookies", "query"]

View File

@@ -1,5 +1,13 @@
import os
import dotenv
from fastapi import FastAPI, APIRouter from fastapi import FastAPI, APIRouter
BASE_DIR: str = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if os.path.exists(os.path.join(BASE_DIR, ".env")):
dotenv.load_dotenv(os.path.join(BASE_DIR, ".env"))
from app.common.security import jwt_security from app.common.security import jwt_security
app = FastAPI() app = FastAPI()

View File

@@ -12,17 +12,17 @@ class SQLAlchemyRepository(AbstractRepository):
async def add_one(self, data: dict) -> int: async def add_one(self, data: dict) -> int:
async with async_session_maker() as session: async with async_session_maker() as session:
stmt = insert(self.model).values(**data).returning(self.model.id) stmt = insert(self.model).values(**data)
res = await session.execute(stmt) result = await session.execute(stmt)
await session.commit() await session.commit()
return res.scalar_one() return result.lastrowid
async def edit_one(self, id: int, data: dict) -> int: async def edit_one(self, id: int, data: dict) -> int:
async with async_session_maker() as session: async with async_session_maker() as session:
stmt = update(self.model).values(**data).filter_by(id=id).returning(self.model.id) stmt = update(self.model).values(**data).filter_by(id=id)
res = await session.execute(stmt) result = await session.execute(stmt)
await session.commit() await session.commit()
return res.scalar_one() return result.lastrowid
async def get_all(self) -> List[BaseReadSchema]: async def get_all(self) -> List[BaseReadSchema]:
async with async_session_maker() as session: async with async_session_maker() as session:
@@ -40,10 +40,10 @@ class SQLAlchemyRepository(AbstractRepository):
async def delete_one(self, id: int) -> int: async def delete_one(self, id: int) -> int:
async with async_session_maker() as session: async with async_session_maker() as session:
stmt = delete(self.model).filter_by(id=id).returning(self.model.id) stmt = delete(self.model).filter_by(id=id)
res = await session.execute(stmt) result = await session.execute(stmt)
await session.commit() await session.commit()
return res.scalar_one() return result.lastrowid
async def find_all(self, **filter_by) -> list[BaseReadSchema]: async def find_all(self, **filter_by) -> list[BaseReadSchema]:
async with async_session_maker() as session: async with async_session_maker() as session:

View File

@@ -0,0 +1,35 @@
services:
app:
build: .
ports: [ 127.0.0.1:8000:8000 ]
env_file:
- .env
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
mysql:
image: mysql:8.4.6
ports: [ 127.0.0.1:3306:3306 ]
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-supersecret}
MYSQL_DATABASE: ${MYSQL_DATABASE:-app}
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: [ "CMD", "mysqladmin", "-u", "root", "--password=${MYSQL_ROOT_PASSWORD:-supersecret}", "ping", "-h", "localhost" ]
timeout: 5s
start_period: 5s
interval: 5s
retries: 10
restart: unless-stopped
redis:
image: redis:8.2.1
ports: [ 127.0.0.1:6379:6379 ]
volumes:
mysql_data:

7
backend/entrypoint.sh Normal file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
set -e
alembic upgrade head
exec "$@"

Binary file not shown.