From 314fbf827b2a69c04712d3e218a3e72dce81f6ff Mon Sep 17 00:00:00 2001 From: MingxuanGame Date: Sun, 10 Aug 2025 08:28:01 +0000 Subject: [PATCH] deploy(docker): support deploy with docker --- .env.example | 9 ++- Dockerfile | 66 ++++++++++++--------- Dockerfile-osurx | 39 +++++++++++++ app/config.py | 11 +++- docker-compose-osurx.yml | 77 ++++++++++++++++++++++++ docker-compose.yml | 122 +++++++++++++++++++++++---------------- docker-entrypoint.sh | 13 +++++ migrations/env.py | 8 +-- requirements.txt | 57 ------------------ 9 files changed, 260 insertions(+), 142 deletions(-) create mode 100644 Dockerfile-osurx create mode 100644 docker-compose-osurx.yml create mode 100644 docker-entrypoint.sh delete mode 100644 requirements.txt diff --git a/.env.example b/.env.example index 47b770c..7715368 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,10 @@ -# 数据库 URL -DATABASE_URL="mysql+aiomysql://root:password@127.0.0.1:3306/osu_api" +# 数据库设置 +MYSQL_HOST="localhost" +MYSQL_PORT=3306 +MYSQL_DATABASE="osu_api" +MYSQL_USER="osu_api" +MYSQL_PASSWORD="password" +MYSQL_ROOT_PASSWORD="password" # Redis URL REDIS_URL="redis://127.0.0.1:6379/0" diff --git a/Dockerfile b/Dockerfile index c1ed5c7..13e59dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,38 @@ -FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim - -WORKDIR /app -ENV UV_PROJECT_ENVIRONMENT=syncvenv - -# 安装系统依赖 -RUN apt-get update && apt-get install -y \ - gcc \ - pkg-config \ - default-libmysqlclient-dev \ - && rm -rf /var/lib/apt/lists/* - -# 复制依赖文件 -COPY uv.lock . -COPY pyproject.toml . -COPY requirements.txt . - -# 安装Python依赖 -RUN pip install -r requirements.txt - -# 复制应用代码 -COPY . . - -# 暴露端口 -EXPOSE 8000 - -# 启动命令 -CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] +FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y \ + gcc \ + pkg-config \ + default-libmysqlclient-dev \ + curl \ + netcat-openbsd \ + && rm -rf /var/lib/apt/lists/* + +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" + +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV UV_PROJECT_ENVIRONMENT=/app/.venv + +COPY pyproject.toml uv.lock ./ +COPY packages/ ./packages/ + +RUN uv sync --frozen --no-dev + +RUN uv pip install rosu-pp-py + +COPY . . + +COPY docker-entrypoint.sh /app/docker-entrypoint.sh +RUN chmod +x /app/docker-entrypoint.sh + +EXPOSE 8000 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +ENTRYPOINT ["/app/docker-entrypoint.sh"] +CMD ["uv", "run", "--no-sync", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/Dockerfile-osurx b/Dockerfile-osurx new file mode 100644 index 0000000..d30cd09 --- /dev/null +++ b/Dockerfile-osurx @@ -0,0 +1,39 @@ +FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y \ + gcc \ + pkg-config \ + default-libmysqlclient-dev \ + curl \ + netcat-openbsd \ + git \ + && rm -rf /var/lib/apt/lists/* + +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" + +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV UV_PROJECT_ENVIRONMENT=/app/.venv + +COPY pyproject.toml uv.lock ./ +COPY packages/ ./packages/ + +RUN uv sync --frozen --no-dev + +RUN uv pip install git+https://github.com/ppy-sb/rosu-pp-py.git + +COPY . . + +COPY docker-entrypoint.sh /app/docker-entrypoint.sh +RUN chmod +x /app/docker-entrypoint.sh + +EXPOSE 8000 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +ENTRYPOINT ["/app/docker-entrypoint.sh"] +CMD ["uv", "run", "--no-sync", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/app/config.py b/app/config.py index e9ea027..f0df438 100644 --- a/app/config.py +++ b/app/config.py @@ -10,9 +10,18 @@ class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") # 数据库设置 - database_url: str = "mysql+aiomysql://root:password@127.0.0.1:3306/osu_api" + mysql_host: str = "localhost" + mysql_port: int = 3306 + mysql_database: str = "osu_api" + mysql_user: str = "osu_api" + mysql_password: str = "password" + mysql_root_password: str = "password" redis_url: str = "redis://127.0.0.1:6379/0" + @property + def database_url(self) -> str: + return f"mysql+aiomysql://{self.mysql_user}:{self.mysql_password}@{self.mysql_host}:{self.mysql_port}/{self.mysql_database}" + # JWT 设置 secret_key: str = Field(default="your-secret-key-here", alias="jwt_secret_key") algorithm: str = "HS256" diff --git a/docker-compose-osurx.yml b/docker-compose-osurx.yml new file mode 100644 index 0000000..159b7da --- /dev/null +++ b/docker-compose-osurx.yml @@ -0,0 +1,77 @@ +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile-osurx + container_name: osu_api_server_osurx + ports: + - "8000:8000" + environment: + - MYSQL_HOST=mysql + - MYSQL_PORT=3306 + - REDIS_URL=redis://redis:6379/0 + - ENABLE_OSU_RX=true + - ENABLE_OSU_AP=true + - ENABLE_ALL_MODS_PP=true + - ENABLE_SUPPORTER_FOR_ALL_USERS=true + - ENABLE_ALL_BEATMAP_LEADERBOARD=true + env_file: + - .env + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + volumes: + - ./replays:/app/replays + - ./static:/app/static + restart: unless-stopped + networks: + - osu-network + + mysql: + image: mysql:8.0 + container_name: osu_api_mysql_osurx + environment: + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} + - MYSQL_DATABASE=${MYSQL_DATABASE} + - MYSQL_USER=${MYSQL_USER} + - MYSQL_PASSWORD=${MYSQL_PASSWORD} + volumes: + - mysql_data:/var/lib/mysql + - ./mysql-init:/docker-entrypoint-initdb.d + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + timeout: 20s + retries: 10 + interval: 10s + start_period: 40s + restart: unless-stopped + networks: + - osu-network + + redis: + image: redis:7-alpine + container_name: osu_api_redis_osurx + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + timeout: 5s + retries: 5 + interval: 10s + start_period: 10s + restart: unless-stopped + networks: + - osu-network + command: redis-server --appendonly yes + +volumes: + mysql_data: + redis_data: + +networks: + osu-network: + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index 8c109c5..9a6f2cb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,50 +1,72 @@ -version: '3.8' - -services: - mysql: - image: mysql:8.0 - container_name: osu_api_mysql - environment: - MYSQL_ROOT_PASSWORD: password - MYSQL_DATABASE: osu_api - MYSQL_USER: osu_user - MYSQL_PASSWORD: osu_password - ports: - - "3306:3306" - volumes: - - mysql_data:/var/lib/mysql - - ./mysql-init:/docker-entrypoint-initdb.d - restart: unless-stopped - - redis: - image: redis:7-alpine - container_name: osu_api_redis - ports: - - "6379:6379" - volumes: - - redis_data:/data - restart: unless-stopped - command: redis-server --appendonly yes - - api: - build: . - container_name: osu_api_server - ports: - - "8000:8000" - environment: - DATABASE_URL: mysql+aiomysql://osu_user:osu_password@mysql:3306/osu_api - REDIS_URL: redis://redis:6379/0 - SECRET_KEY: your-production-secret-key-here - OSU_CLIENT_ID: "5" - OSU_CLIENT_SECRET: "FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk" - depends_on: - - mysql - - redis - restart: unless-stopped - volumes: - - ./:/app - command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload - -volumes: - mysql_data: - redis_data: +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: osu_api_server + ports: + - "8000:8000" + environment: + - MYSQL_HOST=mysql + - MYSQL_PORT=3306 + - REDIS_URL=redis://redis:6379/0 + env_file: + - .env + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + volumes: + - ./replays:/app/replays + - ./static:/app/static + restart: unless-stopped + networks: + - osu-network + + mysql: + image: mysql:8.0 + container_name: osu_api_mysql + environment: + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} + - MYSQL_DATABASE=${MYSQL_DATABASE} + - MYSQL_USER=${MYSQL_USER} + - MYSQL_PASSWORD=${MYSQL_PASSWORD} + volumes: + - mysql_data:/var/lib/mysql + - ./mysql-init:/docker-entrypoint-initdb.d + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + timeout: 20s + retries: 10 + interval: 10s + start_period: 40s + restart: unless-stopped + networks: + - osu-network + + redis: + image: redis:7-alpine + container_name: osu_api_redis + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + timeout: 5s + retries: 5 + interval: 10s + start_period: 10s + restart: unless-stopped + networks: + - osu-network + command: redis-server --appendonly yes + +volumes: + mysql_data: + redis_data: + +networks: + osu-network: + driver: bridge diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..5c51a4b --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -e + +echo "Waiting for database connection..." +while ! nc -z $MYSQL_HOST $MYSQL_PORT; do + sleep 1 +done +echo "Database connected" + +echo "Running alembic..." +uv run --no-sync alembic upgrade head + +exec "$@" diff --git a/migrations/env.py b/migrations/env.py index b8c5c5e..825cde6 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -2,8 +2,8 @@ from __future__ import annotations import asyncio from logging.config import fileConfig -import os +from app.config import settings from app.database import * # noqa: F403 from alembic import context @@ -45,7 +45,8 @@ def run_migrations_offline() -> None: script output. """ - url = os.environ.get("DATABASE_URL", config.get_main_option("sqlalchemy.url")) + url = settings.database_url + print(url) context.configure( url=url, target_metadata=target_metadata, @@ -73,8 +74,7 @@ async def run_async_migrations() -> None: """ sa_config = config.get_section(config.config_ini_section, {}) - if db_url := os.environ.get("DATABASE_URL"): - sa_config["sqlalchemy.url"] = db_url + sa_config["sqlalchemy.url"] = settings.database_url connectable = async_engine_from_config( sa_config, prefix="sqlalchemy.", diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index c0fd664..0000000 --- a/requirements.txt +++ /dev/null @@ -1,57 +0,0 @@ -aiomysql==0.2.0 -alembic==1.16.4 -annotated-types==0.7.0 -anyio==4.9.0 -bcrypt==4.3.0 -certifi==2025.7.14 -cffi==1.17.1 -cfgv==3.4.0 -click==8.2.1 -cryptography==45.0.5 -distlib==0.4.0 -dnspython==2.7.0 -ecdsa==0.19.1 -email-validator==2.2.0 -fastapi==0.116.1 -filelock==3.18.0 -greenlet==3.2.3 -h11==0.16.0 -httpcore==1.0.9 -httptools==0.6.4 -httpx==0.28.1 -identify==2.6.12 -idna==3.10 -loguru==0.7.3 -mako==1.3.10 -markupsafe==3.0.2 -maturin==1.9.2 --e file:///workspaces/osu_lazer_api/packages/msgpack_lazer_api -nodeenv==1.9.1 -passlib==1.7.4 -platformdirs==4.3.8 -pre-commit==4.2.0 -pyasn1==0.6.1 -pycparser==2.22 -pydantic==2.11.7 -pydantic-core==2.33.2 -pymysql==1.1.1 -python-dotenv==1.1.1 -python-jose==3.5.0 -python-multipart==0.0.20 -pyyaml==6.0.2 -redis==6.2.0 -rosu-pp-py==3.1.0 -rsa==4.9.1 -ruff==0.12.4 -six==1.17.0 -sniffio==1.3.1 -sqlalchemy==2.0.41 -sqlmodel==0.0.24 -starlette==0.47.2 -typing-extensions==4.14.1 -typing-inspection==0.4.1 -uvicorn==0.35.0 -uvloop==0.21.0 -virtualenv==20.32.0 -watchfiles==1.1.0 -websockets==15.0.1