diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a298308 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +RUN groupadd --system app && useradd --system --gid app --create-home app + +WORKDIR /app + +COPY pyproject.toml uv.lock ./ +RUN pip install --no-cache-dir uv \ + && uv export --format requirements-txt --no-dev --no-hashes --no-emit-project --frozen -o /tmp/requirements.txt \ + && pip install --no-cache-dir -r /tmp/requirements.txt + +COPY . . +RUN pip install --no-cache-dir -e . +RUN python -m compileall -q encar_scraper +RUN chmod +x entrypoint.sh +RUN chown -R app:app /app + +STOPSIGNAL SIGINT + +USER app + +# По умолчанию API, но worker/beat переопределяют CMD в docker-compose +ENTRYPOINT ["./entrypoint.sh"] +CMD ["uvicorn", "encar_scraper.api.app:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..47bfaf5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,173 @@ +x-app-env: &app-env + # Database (отдельная БД для Encar, порт 5433) + ENCAR_DATABASE_URL: postgresql+psycopg2://encar:encar@postgres:5432/encar_db + ENCAR_DATABASE_POOL_RECYCLE_SECONDS: ${ENCAR_DATABASE_POOL_RECYCLE_SECONDS:-1800} + # Redis / Celery (порт 6380, db=1) + ENCAR_REDIS_URL: ${ENCAR_REDIS_URL:-redis://redis:6379/1} + CELERY_BROKER_URL: ${CELERY_BROKER_URL:-redis://redis:6379/1} + CELERY_RESULT_BACKEND: ${CELERY_RESULT_BACKEND:-redis://redis:6379/1} + CELERY_TASK_SOFT_TIME_LIMIT: ${CELERY_TASK_SOFT_TIME_LIMIT:-14400} + CELERY_TASK_TIME_LIMIT: ${CELERY_TASK_TIME_LIMIT:-14520} + CELERY_BROKER_VISIBILITY_TIMEOUT: ${CELERY_BROKER_VISIBILITY_TIMEOUT:-15000} + CELERY_WORKER_MAX_TASKS_PER_CHILD: ${CELERY_WORKER_MAX_TASKS_PER_CHILD:-50} + # Encar settings + ENCAR_BEAT_INTERVAL_MINUTES: ${ENCAR_BEAT_INTERVAL_MINUTES:-60} + ENCAR_BEAT_LIMIT: ${ENCAR_BEAT_LIMIT:-0} + TZ: ${TZ:-UTC} + +x-env-file: &env-file + - path: .env + required: false + +x-app-service: &app-service + build: . + env_file: *env-file + environment: *app-env + +services: + # PostgreSQL (порт 5434 наружу) + postgres: + image: postgres:16-alpine + container_name: encar-postgres + restart: unless-stopped + environment: + POSTGRES_USER: encar + POSTGRES_PASSWORD: encar + POSTGRES_DB: encar_db + ports: + - "127.0.0.1:5434:5432" + volumes: + - encar_pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U encar -d encar_db"] + interval: 5s + timeout: 3s + retries: 5 + logging: + driver: json-file + options: + max-size: "10m" + max-file: "5" + + # Redis (порт 6380 наружу) + redis: + image: redis:7-alpine + container_name: encar-redis + restart: unless-stopped + ports: + - "127.0.0.1:6380:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + command: > + redis-server + --appendonly yes + --save 60 1000 + volumes: + - encar_redisdata:/data + logging: + driver: json-file + options: + max-size: "10m" + max-file: "5" + + # DB migrations + migrate: + <<: *app-service + container_name: encar-migrate + restart: "no" + depends_on: + postgres: + condition: service_healthy + command: alembic upgrade head + + # FastAPI + api: + <<: *app-service + container_name: encar-api + restart: unless-stopped + ports: + - "127.0.0.1:8001:8000" + depends_on: + migrate: + condition: service_completed_successfully + redis: + condition: service_healthy + command: > + uvicorn encar_scraper.api.app:app + --host 0.0.0.0 --port 8000 --workers 1 + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=5)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 20s + stop_grace_period: 30s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "5" + + # Celery Worker (Encar — только HTTP API, без браузера) + worker: + <<: *app-service + container_name: encar-worker + restart: unless-stopped + init: true + depends_on: + migrate: + condition: service_completed_successfully + redis: + condition: service_healthy + stop_grace_period: 30s + command: > + celery -A encar_scraper.worker.celery_app worker + --loglevel=info --concurrency=4 --pool=prefork + --pidfile=/tmp/celery-worker.pid + -Q encar --max-tasks-per-child=50 + healthcheck: + test: ["CMD-SHELL", "test -f /tmp/celery-worker.pid && kill -0 $(cat /tmp/celery-worker.pid)"] + interval: 60s + timeout: 10s + retries: 3 + start_period: 30s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "5" + + # Celery Beat (периодический планировщик) + beat: + <<: *app-service + container_name: encar-beat + restart: unless-stopped + init: true + depends_on: + migrate: + condition: service_completed_successfully + redis: + condition: service_healthy + command: > + celery -A encar_scraper.worker.celery_app beat + --loglevel=info + --pidfile=/tmp/celery-beat.pid + --schedule=/tmp/celerybeat-schedule + healthcheck: + test: ["CMD-SHELL", "test -f /tmp/celery-beat.pid && kill -0 $(cat /tmp/celery-beat.pid)"] + interval: 60s + timeout: 10s + retries: 3 + start_period: 30s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "5" + +volumes: + encar_pgdata: + encar_redisdata: diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..e5dcf8b --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -euo pipefail + +# Encar scraper — простой entrypoint без браузера. +# Просто запускает переданную команду. + +exec "$@"