From a9fb77c19517f9a68762f284ee147bdf7d6ec040 Mon Sep 17 00:00:00 2001 From: erjemin Date: Wed, 1 Apr 2026 00:08:53 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=84=D0=B8=D0=BD=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=B0=D1=8F=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20production=20Docker=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20RosmorPort=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20Dockerfile:=20-=20=D0=98=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B0=20=D0=B4=D0=BE=D1=81=D1=82=D1=83=D0=BF=D0=B0=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D1=8F=20appuser=20(=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=20COPY)=20-=20=D0=9F?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=80=D1=8F=D0=B4=D0=BE=D0=BA=20=D0=BE=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B9=20(USER=20->=20COPY=20->=20mkdir)=20-?= =?UTF-8?q?=20=D0=9E=D0=BF=D1=82=D0=B8=D0=BC=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B4=D0=BB=D1=8F=20production=20(workers=3D1,=20time?= =?UTF-8?q?out=3D120,=20max-requests=3D200)=20-=20collectstatic=20=D0=B8?= =?UTF-8?q?=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BB?= =?UTF-8?q?=D0=B8=D1=88=D0=BD=D0=B8=D1=85=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE?= =?UTF-8?q?=D0=B2=20=D1=81=D1=82=D0=B0=D1=82=D0=B8=D0=BA=D0=B8=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=20build=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20docker-compose.prod.yml:=20-=20=D0=9F=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B8=D0=BC=D0=B5=D0=BD=D0=BE=D0=B2=D0=B0=D0=BD=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=B9=D0=BD=D0=B5=D1=80=20=D0=B2?= =?UTF-8?q?=20petclones-site--backend=20-=20Production=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=BE=D0=BA?= =?UTF-8?q?=D1=80=D1=83=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20(DEBUG=3DFalse)=20?= =?UTF-8?q?-=20Volumes=20=D0=B4=D0=BB=D1=8F=20media,=20database=20=D0=B8?= =?UTF-8?q?=20nginx=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D0=BE=D0=B2=20-?= =?UTF-8?q?=20Gunicorn=20=D1=81=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5?= =?UTF-8?q?=D1=82=D1=80=D0=B0=D0=BC=D0=B8=20=D0=B4=D0=BB=D1=8F=20productio?= =?UTF-8?q?n=20-=20Watchtower=20=D0=B4=D0=BB=D1=8F=20=D0=B0=D0=B2=D1=82?= =?UTF-8?q?=D0=BE=D0=BC=D0=B0=D1=82=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B7=D0=BE=D0=B2=20-?= =?UTF-8?q?=20=D0=9E=D0=B3=D1=80=D0=B0=D0=BD=D0=B8=D1=87=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D1=80=D0=B5=D1=81=D1=83=D1=80=D1=81=D0=BE=D0=B2=20(0.?= =?UTF-8?q?25=20CPU,=20512M=20RAM)=20-=20JSON=20logging=20=D1=81=20=D1=80?= =?UTF-8?q?=D0=BE=D1=82=D0=B0=D1=86=D0=B8=3F=3F=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20Dockerfile:=20-=20=D0=98=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B0=20=D0=B4=D0=BE=D1=81=D1=82=D1=83=D0=BF=D0=B0=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D1=8F=20appuser=20(=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=20COPY=3F=3F=20=D0=98?= =?UTF-8?q?=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BF?= =?UTF-8?q?=D1=80=3F=3F=20=D0=9F=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D0=BF=D0=BE=D1=80=D1=8F=D0=B4=D0=BE=D0=BA=20?= =?UTF-8?q?=D0=BE=D0=BF=D0=B5=D1=80=D0=B0=D1=86=D0=B8=D0=B9=20(USER=20->?= =?UTF-8?q?=20COPY=20->=20mkdir)=20-=20=D0=9E=D0=BF=D1=82=D0=B8=D0=BC?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B4=D0=BB=D1=8F=20pro?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 18 +---- Dockerfile | 35 +++++--- docker-compose.prod.yml | 175 ++++++++++++++++++---------------------- 3 files changed, 104 insertions(+), 124 deletions(-) diff --git a/.env.example b/.env.example index 03ea2a8..5928542 100644 --- a/.env.example +++ b/.env.example @@ -15,18 +15,6 @@ ADMIN_URL=hidden-admin-panel/ # ======================================== DB_NAME=database/db.sqlite3 -# ======================================== -# Пути для файлов автоматически вычисляются в settings.py -# на основе PROJECT_ROOT (корень проекта) -# ======================================== -# STATIC_ROOT вычисляется как: PROJECT_ROOT / 'public' / 'static' -# MEDIA_ROOT вычисляется как: PROJECT_ROOT / 'public' / 'media' -# Настройки почты (опционально) -# ======================================== -# EMAIL_HOST=smtp.gmail.com -# EMAIL_PORT=587 -# EMAIL_HOST_USER=your-email@gmail.com -# EMAIL_HOST_PASSWORD=your-app-password -# EMAIL_USE_TLS=True -# EMAIL_FROM=noreply@example.com - +# Настройки достпа к пакетам в репозитории, чтобы wathtower мог проверять их свежесть и скачивать +REPO_USER=xxxxx +REPO_PASS=xxxxx diff --git a/Dockerfile b/Dockerfile index b64233e..9453d0e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,14 +39,29 @@ ENV PATH="/opt/venv/bin:$PATH" # Устанавливаем рабочую директорию WORKDIR /app +# Устанавливаем пользователя для запуска приложения (из соображений безопасности) +RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app +USER appuser + # Копируем виртуальное окружение из builder COPY --from=builder /opt/venv /opt/venv # Копируем содержимое проекта -COPY . . +COPY --chown=appuser:appuser . . -# Создаём необходимые директории -RUN mkdir -p /app/public/media /app/public/static /app/database /app/staticfiles +# Создаём директорию для собранной статики и даём права пользователю app +RUN mkdir -p /app/staticfiles && chown -R appuser:appuser /app/staticfiles + +# Создаём директорию для media +RUN mkdir -p /app/public/media && chown -R appuser:appuser /app/public/media + +# Создаём директорию для БД и даём права пользователю app +# Это важно когда БД монтируется как том с хоста +RUN mkdir -p /app/database && chown -R appuser:appuser /app/database + +# Копируем внешний nginx конфиг для экспорта на хост (через volume) +# Это нужно для настройки reverse-proxy на хосте +COPY config/nginx/pet-clones--external-nginx.conf /tmp/pet-clones--external-nginx.conf.source # Собираем статику при сборке образа (для production) # Это гарантирует что статика готова и не нужно запускать collectstatic при старте @@ -68,21 +83,17 @@ RUN find /app/staticfiles/webfonts -type f \( -name "*.ttf" -o -name "*.eot" -o # bootstrap.css, all.css, fontawesome.css используем только .min версии RUN find /app/staticfiles/css -maxdepth 1 -name "*.css" -not -name "*.min.css" -not -name "rosmorport.css" -delete -# Устанавливаем пользователя для запуска приложения (из соображений безопасности) -RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app -USER appuser - # Открываем порт EXPOSE 8000 # CMD - выполняет миграции и запускает Gunicorn # Миграции выполняются при каждом запуске для гибкости # Статика уже собрана при сборке образа -# Параметры оптимизированы для dev с минимальной нагрузкой: +# Параметры оптимизированы для минимальной нагрузкой (ветер качает, но не падает): # - bind 0.0.0.0:8000 (все интерфейсы контейнера, необходимо для Docker проброса портов) -# - workers=1 (один worker - достаточно для dev без нагрузок) -# - timeout=30 (сокращенный таймаут - запросы должны быть быстрыми) -# - max-requests=100 (перезагрузка воркера после 100 запросов - экономия памяти) +# - workers=1 (один worker - достаточно если не будет нагрузок) +# - timeout=120 (сокращенный таймаут - запросы должны быть быстрыми) +# - max-requests=200 (перезагрузка воркера после 200 запросов - экономия памяти) # - access-logfile/error-logfile '-' (логи в stdout для docker) -CMD ["sh", "-c", "cd /app/rosmorport_tsts && echo '>>> Applying database migrations...' && python manage.py migrate --noinput && echo '>>> Starting Gunicorn...' && gunicorn --bind 0.0.0.0:8000 --workers 1 --worker-class sync --worker-tmp-dir /dev/shm --max-requests 100 --timeout 30 --access-logfile - --error-logfile - rosmorport_tsts.wsgi:application"] +CMD ["sh", "-c", "cd /app/rosmorport_tsts && echo '>>> Applying database migrations...' && python manage.py migrate --noinput && echo '>>> Starting Gunicorn...' && gunicorn --bind 0.0.0.0:8000 --workers 1 --worker-class sync --worker-tmp-dir /dev/shm --max-requests 200 --timeout 120 --access-logfile - --error-logfile - rosmorport_tsts.wsgi:application"] diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 0023de0..0dac6a6 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -12,118 +12,99 @@ services: dockerfile: Dockerfile # Имя контейнера - container_name: rosmorport_web_prod + container_name: petclones-site--backend # Переменные окружения для production environment: - DOCKER_ENV=1 - DEBUG=False - - ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0,web,pet-clones.cocorico.ru - - DB_ENGINE=django.db.backends.postgresql - - DB_HOST=db - - DB_PORT=5432 - - DB_NAME=${POSTGRES_DB:-rosmorport_db} - - DB_USER=${POSTGRES_USER:-postgres} - - DB_PASSWORD=${POSTGRES_PASSWORD:-postgres} - PYTHONUNBUFFERED=1 # Тома для медиа, статики и БД volumes: - - public_media:/app/public/media - - public_static:/app/public/static - - database:/app/database - + # Volume для media (загруженные пользователями файлы) + - media:/app/public/media:rw + # Volume для базы данных (SQLite файл) + - database:/app/database:rw + # Volume для экспорта конфигов из контейнера на хост + # Контейнер копирует nginx конфиг при запуске для настройки reverse-proxy на хосте + - ./config/nginx:/tmp/nginx_configs:rw # контейнер пишет сюда конфиги + + # Command: скрипт для копирования конфигов из контейнера на хост при запуске + # Встроенный entrypoint nginx:alpine (/docker-entrypoint.sh) запустит это как команду + command: > + sh -c " + echo '📋 Копирование внешнего nginx конфига на хост...' && + cp /tmp/pet-clones--external-nginx.conf.source /tmp/nginx_configs/pet-clones--external-nginx.conf.sample && + echo '✅ Пример nginx-конфига создан в ./config/nginx/pet-clones--external-nginx.conf.sample (свежий из контейнера)' && + if [ ! -f /tmp/nginx_configs/pet-clones--external-nginx.conf ]; then + cp /tmp/pet-clones--external-nginx.conf.source /tmp/nginx_configs/pet-clones--external-nginx.conf && + echo '✅ Боевой nginx-конфиг создан в ./config/nginx/pet-clones--external-nginx.conf (свежий из контейнера)' + else + echo '⏭️ Боевой nginx-конфиг оставлен без изменений.' + fi + gunicorn --bind 0.0.0.0:8000 --workers 1 --worker-class sync --worker-tmp-dir /dev/shm --max-requests 200 --timeout 120 --access-logfile - --error-logfile - rosmorport_tsts.wsgi:application + " + # Перенаправляем порты ports: - - "8000:8000" - - # Зависимость от базы данных - depends_on: - db: - condition: service_healthy - - # Политика перезапуска - restart: unless-stopped - - # Ограничения ресурсов - deploy: - resources: - limits: - cpus: '1' - memory: 512M - reservations: - cpus: '0.5' - memory: 256M - - # PostgreSQL база данных для production - db: - # Используем официальный образ PostgreSQL - image: postgres:16-alpine - container_name: rosmorport_db_prod - - # Переменные окружения - environment: - - POSTGRES_DB=${POSTGRES_DB:-rosmorport_db} - - POSTGRES_USER=${POSTGRES_USER:-postgres} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres} - - # Том для хранения данных БД - volumes: - - postgres_data:/var/lib/postgresql/data - - # Портов не открываем - доступ только изнутри контейнеров - # ports: - # - "5432:5432" - - # Проверка здоровья сервиса - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"] - interval: 10s - timeout: 5s - retries: 5 - - # Политика перезапуска - restart: unless-stopped - - # Ограничения ресурсов - deploy: - resources: - limits: - cpus: '1' - memory: 1G - reservations: - cpus: '0.5' - memory: 512M - - # Nginx для reverse proxy (опционально) - # nginx: - # image: nginx:alpine - # container_name: rosmorport_nginx - # volumes: - # - ./config/nginx.conf:/etc/nginx/nginx.conf:ro - # - django_staticfiles:/app/static:ro - # - django_mediafiles:/app/media:ro - # ports: - # - "80:80" - # - "443:443" - # depends_on: - # - web - # restart: unless-stopped + - "127.0.0.1:8040:8000" -# Именованные тома для хранения данных -volumes: - postgres_data: - driver: local - public_media: - driver: local - public_static: - driver: local - database: - driver: local + # Политика перезапуска + restart: unless-stopped + + # Метки для Watchtower (авто-обновление) + labels: + - "com.centurylinklabs.watchtower.scope=petclones-site--scope" + + # Ограничения ресурсов + deploy: + resources: + limits: + cpus: '0.25' + memory: 512M + + # Логирование в JSON-файлы (для сбора логов через docker logs) + logging: + driver: "json-file" + options: + max-size: "5m" # больше лимит в продакшене для логирования + max-file: "1" # храним 1 файл лога + + networks: + - petclones-site--network + + # WATCHTOWER ДЛЯ АВТОМАТИЧЕСКОГО ОБНОВЛЕНИЯ ОБРАЗОВ ИЗ РЕЕСТРА + watchtower: + image: containrrr/watchtower + container_name: petclones-site--watchtower + restart: always + volumes: + - /var/run/docker.sock:/var/run/docker.sock + # Переменные окружения + env_file: + - .env + environment: + - DOCKER_API_VERSION=1.44 + # Берем учетные данные из .env файла + - REPO_USER=${REPO_USER} + - REPO_PASS=${REPO_PASS} + # Ограничиваем область видимости только этим проектом + - WATCHTOWER_SCOPE=petclones-site--scope + # Если нужно указать реестр явно (обычно watchtower сам понимает из имени образа) + # - WATCHTOWER_REGISTRY_URL=git.cube2.ru + command: --interval 1800 --cleanup # Проверять каждые 30 минут + logging: + driver: "json-file" + options: + max-size: "1m" + max-file: "1" + networks: + - petclones-site--network # Сеть для сервисов networks: default: - name: rosmorport_network + name: petclones-site--network driver: bridge