diff --git a/.github/workflows/app-actions.yml b/.github/workflows/app-actions.yml index 7c1efe5..8456737 100644 --- a/.github/workflows/app-actions.yml +++ b/.github/workflows/app-actions.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: push: paths: - - '**.py' + - 'app/**' branches: - main - app @@ -19,13 +19,14 @@ jobs: continue-on-error: true steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: 3.12 - architecture: x64 + python-version: "3.12" + cache: pip + cache-dependency-path: app/requirements.txt - name: Install dependencies run: | @@ -42,26 +43,70 @@ jobs: MONGO_PORT: ${{ secrets.MONGO_PORT }} MONGO_USER: ${{ secrets.MONGO_USER }} MONGO_PASS: ${{ secrets.MONGO_PASS }} + RABBIT_HOST: ${{ secrets.RABBIT_HOST }} + RABBIT_PORT: ${{ secrets.RABBIT_PORT }} + RABBIT_USER: ${{ secrets.RABBIT_USER }} + RABBIT_PASS: ${{ secrets.RABBIT_PASS }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: 3.12 - architecture: x64 + python-version: "3.12" + cache: pip + cache-dependency-path: app/requirements.txt - - name: Setup MongoDB - run: docker run --name mongodb -d -p ${{ secrets.MONGO_PORT }}:27017 -e MONGO_INITDB_ROOT_USERNAME=${{ secrets.MONGO_USER }} -e MONGO_INITDB_ROOT_PASSWORD=${{ secrets.MONGO_PASS }} mongodb/mongodb-community-server + - name: Start MongoDB + run: | + docker run -d --name mongodb \ + -p 27017:27017 \ + -e MONGO_INITDB_ROOT_USERNAME=$MONGO_USER \ + -e MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASS \ + mongodb/mongodb-community-server + for i in $(seq 1 30); do + docker exec mongodb mongosh \ + --username $MONGO_USER --password $MONGO_PASS \ + --eval "db.runCommand({ping:1})" && break + sleep 1 + done + + - name: Start RabbitMQ + run: | + docker run -d --name rabbitmq \ + -p ${{ secrets.RABBIT_PORT }}:5672 \ + -e RABBITMQ_DEFAULT_USER=${{ secrets.RABBIT_USER }} \ + -e RABBITMQ_DEFAULT_PASS=${{ secrets.RABBIT_PASS }} \ + rabbitmq:3.13-alpine + for i in $(seq 1 30); do + docker exec rabbitmq rabbitmq-diagnostics -q ping && break + sleep 1 + done - name: Install dependencies run: | python -m pip install --upgrade pip pip install pytest==8.3.4 pytest-asyncio==0.25.3 httpx==0.28.1 pip install -r app/requirements.txt - - name: Setup moderated base cards + + - name: Setup moderated base cards working-directory: ./app/tools run: python3 _add_base_cards.py -a 2 -f data/base_cards.json + - name: Run pytest - run: pytest -vs \ No newline at end of file + run: pytest -vs + + docker-build: + runs-on: ubuntu-latest + needs: [mypy, pytest] + if: github.ref == 'refs/heads/main' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build backend image + run: docker build -f app/dockerfile.app -t tort-backend:ci ./app + + - name: Build bot image + run: docker build -f app/dockerfile.bot -t tort-tg-bot:ci ./app \ No newline at end of file diff --git a/app/.dockerignore b/app/.dockerignore new file mode 100644 index 0000000..4016b01 --- /dev/null +++ b/app/.dockerignore @@ -0,0 +1,15 @@ +__pycache__/ +*.pyc +*.pyo +.pytest_cache/ +.venv/ +.env +tests/ +security.log +.git/ +.github/ +*.md +dockerfile.app +dockerfile.bot +.dockerignore + diff --git a/app/dockerfile b/app/dockerfile deleted file mode 100644 index 281fd13..0000000 --- a/app/dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM python:3.9.21-alpine - -WORKDIR /app - -COPY . . - -RUN pip3 install -r requirements.txt - -EXPOSE 5000 - -CMD ["python3", "main.py"] \ No newline at end of file diff --git a/app/dockerfile.app b/app/dockerfile.app new file mode 100644 index 0000000..2157f69 --- /dev/null +++ b/app/dockerfile.app @@ -0,0 +1,32 @@ +# ── Stage 1: Install dependencies ──────────────────────────── +FROM python:3.12.4-slim AS builder + +WORKDIR /build + +COPY requirements.txt . +RUN pip install --no-cache-dir --prefix=/install -r requirements.txt + +# ── Stage 2: Production image ──────────────────────────────── +FROM python:3.12.4-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app + +# Copy installed packages from builder +COPY --from=builder /install /usr/local + +# Copy application code (respects .dockerignore) +COPY . . + +# Create non-root user +RUN groupadd --gid 1000 appuser && \ + useradd --uid 1000 --gid appuser --shell /bin/sh appuser && \ + chown -R appuser:appuser /app + +USER appuser + +EXPOSE 5000 + +CMD ["python3", "main.py"] \ No newline at end of file diff --git a/app/dockerfile.bot b/app/dockerfile.bot new file mode 100644 index 0000000..4dec4c8 --- /dev/null +++ b/app/dockerfile.bot @@ -0,0 +1,34 @@ +# ── Stage 1: Install dependencies ──────────────────────────── +FROM python:3.12.4-slim AS builder + +WORKDIR /build + +COPY requirements.txt . +RUN pip install --no-cache-dir --prefix=/install -r requirements.txt + +# ── Stage 2: Production image ──────────────────────────────── +FROM python:3.12.4-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app + +# Copy installed packages from builder +COPY --from=builder /install /usr/local + +# Copy application code (respects .dockerignore) +COPY . . + +# Create non-root user +RUN groupadd --gid 1000 appuser && \ + useradd --uid 1000 --gid appuser --shell /bin/sh appuser && \ + chown -R appuser:appuser /app + +USER appuser + +# Healthcheck: verify RabbitMQ connection is possible +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD python3 -c "import socket; s=socket.create_connection(('${RABBIT_HOST:-rabbitmq}', int('${RABBIT_PORT:-5672}')), timeout=3); s.close()" || exit 1 + +CMD ["python3", "tg_bot.py"] diff --git a/docker-compose.yml b/docker-compose.yml index c21f383..06a4be9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,34 +1,93 @@ -version: '3.8' - services: mongodb: image: mongodb/mongodb-community-server container_name: tort-mongodb restart: always - network_mode: bridge environment: MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER} MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASS} ports: - "127.0.0.1:${MONGO_PORT}:27017" + networks: + - tort-net healthcheck: - test: ["CMD", "mongosh", "--username", "${MONGO_USER}", "--password", "${MONGO_PASS}", "--eval", "db.runCommand({ ping: 1 })"] + test: [ "CMD", "mongosh", "--username", "${MONGO_USER}", "--password", "${MONGO_PASS}", "--eval", "db.runCommand({ ping: 1 })" ] interval: 10s timeout: 5s - retries: 2 + retries: 3 + + rabbitmq: + image: rabbitmq:3.13-management-alpine + container_name: tort-rabbitmq + restart: always + environment: + RABBITMQ_DEFAULT_USER: ${RABBIT_USER} + RABBITMQ_DEFAULT_PASS: ${RABBIT_PASS} + ports: + - "127.0.0.1:5672:5672" + - "127.0.0.1:15672:15672" + networks: + - tort-net + healthcheck: + test: [ "CMD", "rabbitmq-diagnostics", "-q", "ping" ] + interval: 10s + timeout: 5s + retries: 3 backend: build: context: ./app + dockerfile: dockerfile.app image: tort-backend:latest + container_name: tort-backend + restart: always depends_on: mongodb: condition: service_healthy - container_name: tort-backend - network_mode: "host" + rabbitmq: + condition: service_healthy environment: - MONGO_HOST: ${MONGO_HOST} - MONGO_PORT: ${MONGO_PORT} + MONGO_HOST: mongodb + MONGO_PORT: "27017" MONGO_USER: ${MONGO_USER} MONGO_PASS: ${MONGO_PASS} + RABBIT_HOST: rabbitmq + RABBIT_PORT: "5672" + RABBIT_USER: ${RABBIT_USER} + RABBIT_PASS: ${RABBIT_PASS} + MODERATION_SECRET: ${MODERATION_SECRET} + DISABLE_DOCS: ${DISABLE_DOCS:-true} + LOG_LEVEL: ${LOG_LEVEL:-INFO} + ports: + - "127.0.0.1:5000:5000" + networks: + - tort-net + tg-bot: + build: + context: ./app + dockerfile: dockerfile.bot + image: tort-tg-bot:latest + container_name: tort-tg-bot + restart: always + depends_on: + rabbitmq: + condition: service_healthy + backend: + condition: service_started + environment: + TG_BOT_TOKEN: ${TG_BOT_TOKEN} + TG_ADMIN_CHAT_ID: ${TG_ADMIN_CHAT_ID} + API_BASE_URL: http://backend:5000 + MODERATION_SECRET: ${MODERATION_SECRET} + RABBIT_HOST: rabbitmq + RABBIT_PORT: "5672" + RABBIT_USER: ${RABBIT_USER} + RABBIT_PASS: ${RABBIT_PASS} + LOG_LEVEL: ${LOG_LEVEL:-INFO} + networks: + - tort-net + +networks: + tort-net: + driver: bridge