From 5d7470ac7d76c3c4f91da1f2c05677a323c4cd9f Mon Sep 17 00:00:00 2001 From: erjemin Date: Tue, 14 Apr 2026 16:28:12 +0300 Subject: [PATCH] mod: update production docker deployment --- .dockerignore | 12 +- .gitea/workflows/docker-publish.yaml | 72 +++++++++ .../nginx/cadpoint-app--external-nginx.conf | 147 +++++++++++++++++ docker-compose.prod.yml | 148 ++++++++++++++++++ 4 files changed, 373 insertions(+), 6 deletions(-) create mode 100644 .gitea/workflows/docker-publish.yaml create mode 100644 config/nginx/cadpoint-app--external-nginx.conf create mode 100644 docker-compose.prod.yml diff --git a/.dockerignore b/.dockerignore index 946b975..49d83c1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,6 +9,7 @@ # Секреты и локальные настройки не должны попадать в контейнерный контекст. .env .env.* +.env.sample # Виртуальное окружение и служебные артефакты Python. .venv/ @@ -25,6 +26,7 @@ htmlcov/ # Локальные базы и дампы SQLite в контейнер не тащим. *.sqlite3 database/ +media/ # Локальная сборка фронтенда пока не нужна в Docker-контексте. # Если позже соберём frontend внутри Docker, это правило можно пересмотреть. @@ -37,10 +39,8 @@ public/media/ *.md **/.gitignore -# Будущие Dockerfile и основной compose-файл обычно храним в репозитории, -# поэтому их НЕ игнорируем. Игнорируем только локальные override-варианты. -docker-compose.override.yml -compose.override.yml -docker-compose.local.yml -compose.local.yml +# Репозиторные и оркестрационные файлы не нужны внутри runtime-образа. +.gitea/ +Dockerfile +docker-compose*.yml diff --git a/.gitea/workflows/docker-publish.yaml b/.gitea/workflows/docker-publish.yaml new file mode 100644 index 0000000..0422340 --- /dev/null +++ b/.gitea/workflows/docker-publish.yaml @@ -0,0 +1,72 @@ +name: Build and Push Docker Image +run-name: Build and Push Docker Image ${{ github.ref_name }} + +on: + push: + # Запускать сборку только при создании тега, начинающегося с 'v' (например, v1.0.0, v2.3.1) + tags: + - 'v*' + +env: + REGISTRY: git.cube2.ru + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest # Или метка вашего раннера, если он специфичный (например, macos или self-hosted) + container: + image: catthehacker/ubuntu:act-latest + + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Настройка QEMU для мультиплатформенной сборки (если нужно собирать под разные архитектуры) + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + # Настройка Docker Buildx (обязательно для build-push-action) + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + # Логин в реестр Gitea + - name: Log in to the Container registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.REPO_USER }} + password: ${{ secrets.REPO_PASS }} + + # Извлечение метаданных (тегов и лейблов) для Docker + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=tag + type=raw,value=latest,enable=${{ github.ref_type == 'tag' }} + + # Сборка и отправка образа + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: . + file: Dockerfile + push: true + # Собираем под текущую архитектуру (linux/amd64). + # Если сервер и MacMini на разных архитектурах (x86 vs ARM), добавьте нужные, например: linux/amd64,linux/arm64 + # platforms: linux/amd64,linux/arm64 + # --- + # Собираем только под linux/amd64 (для скорости) + platforms: linux/amd64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + # ДОБАВЛЕНО для медленного интернета и оптимизации сборки: + cache-from: type=gha + cache-to: type=gha,mode=max + timeout: 900 # 15 минут на всю сборку diff --git a/config/nginx/cadpoint-app--external-nginx.conf b/config/nginx/cadpoint-app--external-nginx.conf new file mode 100644 index 0000000..e8b2eee --- /dev/null +++ b/config/nginx/cadpoint-app--external-nginx.conf @@ -0,0 +1,147 @@ +# config/nginx/cadpoint-app--external-nginx.conf +# ============================================================================== +# ЭТАЛОННЫЙ КОНФИГУРАЦИОННЫЙ ФАЙЛ NGINX (Reverse Proxy для Docker) +# ============================================================================== +# +# ВНИМАНИЕ: +# Этот файл является шаблоном. При первом деплое он копируется в `/home/user/app/cadpoint-site/config/nginx/cadpoint-app--external-nginx.conf`, +# а затем (уже руками) через силинк в `/etc/nginx/sites-available/` и активируется. +# При последующих деплоях он НЕ ПЕРЕЗАПИСЫВАЕТСЯ автоматически, чтобы не затереть SSL-сертификаты и ручные правки. +# +# Если вы изменили этот файл в репозитории и хотите применить изменения на проде: +# вам нужно обновить файл в `/home/user/app/cadpoint-site/config/nginx/cadpoint-app--external-nginx.conf` вручную (diff + copy). +# +# Так же (рядом) будет создан образец этого файла `nginx_cadpoint.conf.example`, который будет обновляться при деплоях +# из репозитория, чтобы вы могли видеть, что изменилось и при необходимости перенести эти изменения на прод. +# +# Предполагаемая структура на сервере: +# /home/user/app/cadpoint-site/ +# ├── docker-compose.yml +# ├── .env +# ├── media/ <-- Сюда Nginx смотрит напрямую (Docker volume) +# └── ... + +# 1. Описываем, где живет наш Django в Docker +upstream cadpoint-django { + # Мы пробрасываем порт 8050 из контейнера наружу (в docker-compose.yml имя сервиса 'web', контейнер 'cadpoint-backend') + server 127.0.0.1:8050; + keepalive_requests 200; +} + +# 2. Конфигурируем сервер +server { + server_name test.cadpoint.ru; # Основное доменное имя + + # Слушаем 80 порт (Certbot потом добавит сюда редирект на 443 и настройки SSL) + listen 80; + listen [::]:80; + + charset utf-8; + client_max_body_size 10M; # Разрешаем загрузку не слишком больших картинок + + # Логи (пути могут отличаться в зависимости от настроек сервера, здесь стандартные для Ubuntu) + access_log /var/log/nginx/cadpoint.access.log; + error_log /var/log/nginx/cadpoint.error.log; + + # --- GZIP (Сжатие) --- + # Очень важно для динамического HTML от Django, который Gunicorn отдает несжатым. + gzip on; + gzip_vary on; # Добавляет заголовок Vary: Accept-Encoding + gzip_proxied any; # Сжимать ответы, даже если мы за прокси + gzip_comp_level 6; # Оптимальный баланс скорость/сжатие + gzip_min_length 1000; # Не сжимать совсем мелочь + # Типы файлов для сжатия (HTML сжимается автоматически, его писать не нужно) + gzip_types + text/plain + text/css + text/xml + text/javascript + application/javascript + application/json + application/xml + application/xml+rss + image/svg+xml + image/x-icon + application/vnd.ms-fontobject + font/woff + font/woff2; + + # --- МЕДИА ФАЙЛЫ (Загруженный контент) --- + # Nginx отдает их напрямую с диска хоста, не дергая Docker. + # Путь должен совпадать с тем, где лежит volume на хост-машине. + # ВАЖНО: Убедитесь, что пользователь nginx (www-data) имеет права на чтение этой папки! + # ТРЕБУЕТСЯ ЗАМЕНА ПРИ ДЕПЛОЕ: /home/user/app/cadpoint-site -> ваш реальный путь + location /media/ { + alias /home/user/app/cadpoint-site/media/; + expires 30d; # Кешируем картинки на месяц + add_header Cache-Control "public, no-transform"; + } + + # --- СТРАНИЦЫ ОШИБОК (Custom Error Pages) --- + # Если Django упал (502) или сработал тайм-аут (504), Nginx должен отдать статический HTML. + # Эти файлы должны лежать в папке, доступной Nginx (например, в `media/_error`). + # + # ВАЖНО: + # 1. Файлы 50x.html (500, 502, 503, 504) копируются в `media/_error` при старте контейнера (см. docker-compose.prod.yml -> command). + # 2. error_page директива перехватывает ошибки от апстрима (Gunicorn). + error_page 500 /500.html; + error_page 502 /502.html; + error_page 503 /503.html; + error_page 504 /504.html; + + location = /500.html { root /home/user/app/cadpoint-site/media/_error; internal; } + location = /502.html { root /home/user/app/cadpoint-site/media/_error; internal; } + location = /503.html { root /home/user/app/cadpoint-site/media/_error; internal; } + location = /504.html { root /home/user/app/cadpoint-site/media/_error; internal; } + + # 404 (и другие) тоже нужно кастомизировать... обычно Django сам отдает 404. + # Но, например, Nginx отдаст 404 при ошике доступа к media-файлам (они храняться на хосте, а не в контейнере) + error_page 400 /400.html; + error_page 401 /401.html; + error_page 403 /403.html; + error_page 404 /404.html; + error_page 413 /413.html; + error_page 429 /429.html; + + location = /400.html { root /home/user/app/cadpoint-site/media/_error; internal; } + location = /401.html { root /home/user/app/cadpoint-site/media/_error; internal; } + location = /403.html { root /home/user/app/cadpoint-site/media/_error; internal; } + location = /404.html { root /home/user/app/cadpoint-site/media/_error; internal; } + location = /413.html { root /home/user/app/cadpoint-site/media/_error; internal; } + location = /429.html { root /home/user/app/cadpoint-site/media/_error; internal; } + + # Указываем единую страницу (на реконструкции) для всех прочих ошибок + error_page 405 406 407 408 409 410 411 412 414 415 416 417 418 421 422 423 424 425 426 428 431 451 /under_reconstruction.html; + location = /under_reconstruction.html { root /home/user/app/cadpoint-site/media/_error; internal; } + + # --- ВСЁ ОСТАЛЬНОЕ (Django + WhiteNoise) --- + # Статика (/static/), robots.txt, favicon.ico и сам сайт обрабатываются внутри контейнера. + # Nginx просто прокидывает запрос внутрь. + location / { + proxy_pass http://cadpoint-django; + + # Передаем правильные заголовки, чтобы Django знал реальный IP и протокол + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # Если нужно чтобы Django обрабатывал и HTTP, и HTTPS, то можно раскомментировать эту строку + # и передавать реальный протокол от клиента + # proxy_set_header X-Forwarded-Proto $scheme; + + # Явно указываем https, потому что клиент всегда приходит по HTTPS к Nginx + # Даже если внутри контейнера это HTTP на 127.0.0.1:8050, для Django это должно быть HTTPS + proxy_set_header X-Forwarded-Proto https; + + # Тайм-ауты (важно для долгих операций, если они есть) + proxy_read_timeout 180s; + proxy_connect_timeout 180s; + } +} + +# 3. Редирект с www на без-www (SEO best practice) +# server { +# server_name www.cadpoint.ru; +# listen 80; +# return 301 $scheme://cadpoint.ru$request_uri; # Всегда редиректим на основной домен +# } \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..474fb1e --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,148 @@ +# ============================================================================== +# Docker Compose для PRODUCTION +# Этот файл запускается на боевом сервере. +# Вариант 1 (если переименовали в docker-compose.yml): docker compose up -d +# Вариант 2 (если оставили имя): docker compose -f docker-compose.prod.yml up -d +# ============================================================================== + +# В новой версии Docker не нужно +# version: '3.8' + +services: + # --- ОCНОВНОЙ СЕРВИС: DJANGO + GUNICORN + WHITENOISE --- + web: + # Имя контейнера + container_name: cadpoint-backend + + # 1. ОБРАЗ + # В продакшене мы используем готовый, собранный образ из реестра (Gitea) + image: git.cube2.ru/erjemin/2021-cadpoint-ru:latest + # Если образа в gitae нет, то перенести весь код в прод и можно собирать локально: + # build: . + + restart: always + + # 2. Метки для Watchtower (авто-обновление) + labels: + - "com.centurylinklabs.watchtower.scope=cadpoint-scope" + + # 3. КОМАНДА ЗАПУСКА (Замена entrypoint.sh) + # Выполняем цепочку команд внутри контейнера при запуске: + # a. Миграции + # b. Collectstatic + # с. Создаем папку nginx в примонтированном томе конфигов (если нет) + # d. Копирование конфига Nginx с авто-заменой путей через sed (замену реального пути на хосте получаем + # через переменную окружения HOST_PROJECT_PATH) + # e. Инициализация боевого конфига (если нет) + # f. Создаем папку для ошибок и копируем туда HTML error pages и их ассеты (там их увидит Nginx хоста) + # — иконки, SVG-иллюстрации и under_reconstruction тоже должны лежать рядом, чтобы Nginx мог их отдать + # g. Запуск Gunicorn + command: > + sh -c "python manage.py migrate --noinput && + python manage.py collectstatic --noinput --clear && + mkdir -p /nginx_configs_host/nginx && + sed \"s|/home/user/app/cadpoint-site|${HOST_PROJECT_PATH:-/home/default_user/projects/cadpoint-site}|g\" /nginx_configs_host/nginx/cadpoint-app--external-nginx.conf > /nginx_configs_host/nginx/nginx_cadpoint.conf.example && + if [ ! -f /nginx_configs_host/nginx/cadpoint-app--external-nginx.conf ]; then + cp /nginx_configs_host/nginx/nginx_cadpoint.conf.example /nginx_configs_host/nginx/cadpoint-app--external-nginx.conf; + echo 'INIT: Created new nginx config with correct paths'; + fi && + ERROR_DIR=/home/app/web/public/media/_error && + mkdir -p "$$ERROR_DIR/svgs" "$$ERROR_DIR/img" && + for code in 400 401 403 404 413 429 500 502 503 504; do + cp /home/app/web/cadpoint/templates/$${code}.html "$$ERROR_DIR/$${code}.html"; + done && + cp /home/app/web/cadpoint/templates/under_reconstruction.html "$$ERROR_DIR/under_reconstruction.html" && + cp /home/app/web/public/static/svgs/favicon.svg "$$ERROR_DIR/svgs/favicon.svg" && + cp /home/app/web/public/static/svgs/xxx-error.svg "$$ERROR_DIR/svgs/xxx-error.svg" && + cp /home/app/web/public/static/svgs/404-error.svg "$$ERROR_DIR/svgs/404-error.svg" && + cp /home/app/web/public/static/svgs/500-error.svg "$$ERROR_DIR/svgs/500-error.svg" && + cp /home/app/web/public/static/svgs/cappoint_under_reconstruction.svg "$$ERROR_DIR/svgs/cappoint_under_reconstruction.svg" && + cp /home/app/web/public/static/img/favicon.png "$$ERROR_DIR/img/favicon.png" && + cp /home/app/web/public/static/img/favicon.ico "$$ERROR_DIR/img/favicon.ico" && + gunicorn --workers 2 --bind 0.0.0.0:8000 cadpoint.wsgi:application" + + # 4. Проброс портов (Внешний Nginx -> localhost:8050) + ports: + # Слушаем только на localhost хоста, чтобы закрыть прямой доступ из интернета к Gunicorn + - "127.0.0.1:8050:8000" + + # 5. Тома (Volumes) + volumes: + # База данных + # Монтируем папку database с хоста в папку с базой внутри контейнера. + # Путь в контейнере: /home/app/web/database (так как Django ищет базу в BASE_DIR.parent/database) + - ./database:/home/app/web/database + + # Медиа, служебные error-pages и их ассеты лежат в папке `media` на хосте. + - ./media:/home/app/web/public/media + + # Конфиги (Монтируем папку ./config с хоста в /nginx_configs_host внутри контейнера) + # Это нужно, чтобы скрипт запуска мог положить туда .example конфиг и прочитать боевой конфиг. + - ./config:/nginx_configs_host + + # 6. Пользователь и права + user: "1000:1000" + + # Когда нужна отладка процессов внутри контейнера, можно временно раскомментировать эту строку и запустить контейнер с правами root. + # cap_add: + # - SYS_PTRACE + + # 7. Переменные окружения + env_file: + - .env + environment: + - DJANGO_SETTINGS_MODULE=cadpoint.settings + - PYTHONUNBUFFERED=1 + # Передаем переменную с путем на хосте внутрь контейнера, чтобы sed мог её использовать + - HOST_PROJECT_PATH=${HOST_PROJECT_PATH:-/home/default_user/projects/cadpoint-site} + + # 8. Проверка здоровья контейнера (Healthcheck) + # Docker будет периодически проверять статус контейнера. Это критично для Watchtower! + # Если контейнер объявлен "unhealthy", Watchtower сначала остановит старый образ, потом запустит новый. + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/').read()"] + interval: 3m # Проверка каждые 3 минуты + timeout: 12s # Таймаут ответа - 12 секунды + start_period: 20s # Даем 20 секунд на стартап перед первой проверкой + retries: 3 # Unhealthy после 3 неудачных попыток + + # 9. Логирование (Ротация) + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # 10. Ресурсы + deploy: + resources: + limits: + cpus: '0.50' + memory: 512M + mem_limit: 512m + + # --- WATCHTOWER: АВТО-ОБНОВЛЕНИЕ ОБРАЗОВ --- + # Следит за реестром Gitea и обновляет контейнер web, если появился новый image + watchtower: + image: containrrr/watchtower + container_name: cadpoint_watchtower + restart: always + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + # Токен/Логин для вашего приватного реестра (нужно добавить в .env!) + # REPO_USER и REPO_PASS должны быть в .env файле на сервере + - REPO_USER=${REPO_USER} + - REPO_PASS=${REPO_PASS} + - WATCHTOWER_SCOPE=cadpoint-scope + - WATCHTOWER_CLEANUP=true # Удалять старые образы после обновления + - DOCKER_API_VERSION=1.44 + # Дополнительные опции для правильной работы с healthcheck + - WATCHTOWER_WAIT_ON_TIMEOUT=60 # Ждем 60 сек пока контейнер станет healthy перед финализацией + - WATCHTOWER_LIFECYCLE_HOOKS=true # Включаем lifecycle hooks для graceful shutdown + command: --interval 1800 --cleanup # Проверять каждые 30 минут + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" \ No newline at end of file