From d8644cf921fd33cf9010b35be03952e1fee7b79c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0=20=D0=9F=D0=BE=D1=82?= =?UTF-8?q?=D0=B0=D0=BF=D0=BE=D0=B2?= Date: Fri, 29 Aug 2025 16:49:46 +0400 Subject: [PATCH] Docker Compose deploy ready --- backend/.env.example | 23 ++++++++ backend/Dockerfile | 53 ++++++++++++++++++ backend/app/common/security.py | 2 +- backend/app/config.py | 43 +++++++++----- backend/app/main.py | 8 +++ .../app/repositories/sqlalchemy_repository.py | 18 +++--- backend/compose.yml | 35 ++++++++++++ backend/entrypoint.sh | 7 +++ backend/requirements.txt | Bin 2130 -> 2196 bytes 9 files changed, 165 insertions(+), 24 deletions(-) create mode 100644 backend/.env.example create mode 100644 backend/entrypoint.sh diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..6d15a8b --- /dev/null +++ b/backend/.env.example @@ -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 diff --git a/backend/Dockerfile b/backend/Dockerfile index e69de29..78de8d4 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -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"] diff --git a/backend/app/common/security.py b/backend/app/common/security.py index 9b3d148..3235b7a 100644 --- a/backend/app/common/security.py +++ b/backend/app/common/security.py @@ -11,7 +11,7 @@ def get_password_hash(password: str) -> str: jwt_config = AuthXConfig( 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, ) diff --git a/backend/app/config.py b/backend/app/config.py index 18cb726..e033e61 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -1,19 +1,34 @@ -database_url = "sqlite+aiosqlite:///data/database.sqlite" +import os -smtp_host = "smtp.beget.com" -smtp_port = 2525 -smtp_username = "university@nspotapov.ru" -smtp_from = "university@nspotapov.ru" -smtp_password = "V%P2WUe2wnwL" +database_driver = os.getenv("DATABASE_DRIVER") +database_host = os.getenv("DATABASE_HOST") +database_port = int(os.getenv("DATABASE_PORT")) +database_name = os.getenv("DATABASE_NAME") +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" -redis_port = 6379 -redis_username = None -redis_password = None -redis_db = 0 +smtp_host = os.getenv("SMTP_HOST") +smtp_port = os.getenv("SMTP_PORT") +smtp_username = os.getenv("SMTP_USERNAME") +smtp_from = os.getenv("SMTP_FROM") +smtp_password = os.getenv("SMTP_PASSWORD") -jwt_algorithm = "HS256" -swt_secret_key = "SECRET_KEY" +otp_code_expired_time = int(os.getenv("OTP_CODE_EXPIRED_TIME", "5")) # minutes + +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"] diff --git a/backend/app/main.py b/backend/app/main.py index a4e6d73..154e104 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,5 +1,13 @@ +import os + +import dotenv 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 app = FastAPI() diff --git a/backend/app/repositories/sqlalchemy_repository.py b/backend/app/repositories/sqlalchemy_repository.py index 843e6c3..241b605 100644 --- a/backend/app/repositories/sqlalchemy_repository.py +++ b/backend/app/repositories/sqlalchemy_repository.py @@ -12,17 +12,17 @@ class SQLAlchemyRepository(AbstractRepository): async def add_one(self, data: dict) -> int: async with async_session_maker() as session: - stmt = insert(self.model).values(**data).returning(self.model.id) - res = await session.execute(stmt) + stmt = insert(self.model).values(**data) + result = await session.execute(stmt) await session.commit() - return res.scalar_one() + return result.lastrowid async def edit_one(self, id: int, data: dict) -> int: async with async_session_maker() as session: - stmt = update(self.model).values(**data).filter_by(id=id).returning(self.model.id) - res = await session.execute(stmt) + stmt = update(self.model).values(**data).filter_by(id=id) + result = await session.execute(stmt) await session.commit() - return res.scalar_one() + return result.lastrowid async def get_all(self) -> List[BaseReadSchema]: async with async_session_maker() as session: @@ -40,10 +40,10 @@ class SQLAlchemyRepository(AbstractRepository): async def delete_one(self, id: int) -> int: async with async_session_maker() as session: - stmt = delete(self.model).filter_by(id=id).returning(self.model.id) - res = await session.execute(stmt) + stmt = delete(self.model).filter_by(id=id) + result = await session.execute(stmt) await session.commit() - return res.scalar_one() + return result.lastrowid async def find_all(self, **filter_by) -> list[BaseReadSchema]: async with async_session_maker() as session: diff --git a/backend/compose.yml b/backend/compose.yml index e69de29..69c989d 100644 --- a/backend/compose.yml +++ b/backend/compose.yml @@ -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: \ No newline at end of file diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh new file mode 100644 index 0000000..df7bb53 --- /dev/null +++ b/backend/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -e + +alembic upgrade head + +exec "$@" diff --git a/backend/requirements.txt b/backend/requirements.txt index 5ca5a48bbcb6c44c75f6685cab87f52c9b029653..bb4a36f0aec4b604999ffb2ffaf191e2eb912756 100644 GIT binary patch delta 78 zcmca4Fhx-1|Gz|rOon`hT!u=9VunJ790pq;G+@wUFalx&23`g(hK)=DtkMBM1-?KW V%n-=n16E`RQ~<(8n|WDXnE|YK4sZYf delta 16 XcmbOtcu9ci|G$kY0<4=uSRI%FINSxm