From 0773b25064ce036289f4ed996b0a890097ca7d76 Mon Sep 17 00:00:00 2001 From: erjemin Date: Sun, 13 Sep 2026 21:08:08 +0300 Subject: [PATCH] =?UTF-8?q?add:=20=D1=80=D0=B0=D0=B7=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D1=82=D1=8B=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20(1)=20-=20=D0=B2?= =?UTF-8?q?=20dev-=D0=BE=D0=BA=D1=80=D1=83=D0=B6=D0=B5=D0=BD=D0=B8=D0=B8?= =?UTF-8?q?=20=D0=B2=D1=81=D0=B5=20=D0=BE=D0=BA!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 51 +++++++ .env.sample | 12 +- .gitea/workflows/docker-publish.yaml | 74 ++++++++++ Dockerfile | 99 +++++++++++++ config/nginx/hypn0-app--external-nginx.conf | 147 ++++++++++++++++++++ docker-compose.dev.yml | 69 +++++++++ hypn0/hypn0/urls.py | 4 +- 7 files changed, 451 insertions(+), 5 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/docker-publish.yaml create mode 100644 Dockerfile create mode 100644 config/nginx/hypn0-app--external-nginx.conf create mode 100644 docker-compose.dev.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3ce162b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,51 @@ +# Исключаем мусор и локальные артефакты, чтобы Docker-контекст был компактным. + +# Git и IDE-файлы в образ не нужны. +.git +.github +.idea +.DS_Store + +# Секреты и локальные настройки не должны попадать в контейнерный контекст. +.env +.env.* +.env.sample + +# Виртуальное окружение и служебные артефакты Python. +.venv/ +__pycache__/ +*.py[cod] +*.log +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage* +htmlcov/ +.tox/ + +# Локальные базы и дампы SQLite в контейнер не тащим. +*.sqlite3 +database/ +media/ + +# Локальная сборка фронтенда пока не нужна в Docker-контексте. +# Если позже соберём frontend внутри Docker, это правило можно пересмотреть. +frontend-assembly/ + +# Загруженные медиа-файлы монтируются отдельно и не должны раздувать контекст. +public/media/ + +# Документация и служебные git-ignore-файлы не нужны в runtime-образе. +*.md +**/.gitignore + +# Репозиторные и оркестрационные файлы не нужны внутри runtime-образа. +.gitea/ +.junie/ +_blueprint/ +docker-compose*.yml + +# В принцепе, Dockerfile тоже можно исключить из контейнера, но для BuildKit это иногда ломает сборку +# и считается антипаттерном, а так же может приводить к предупреждениям при сборке. +# Dockerfile + diff --git a/.env.sample b/.env.sample index d7a4905..e364849 100644 --- a/.env.sample +++ b/.env.sample @@ -2,10 +2,16 @@ # Скопируй этот файл в `.env` и заполни реальными значениями. DJANGO_DEBUG=True -DJANGO_SECRET_KEY=CHANGE_ME -DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost,hypn0.ru +DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost,hypn0.xyz DJANGO_ADMINS=hypn0:admin@example.com -DJANGO_CSRF_TRUSTED_ORIGINS=http://127.0.0.1:8000,http://localhost:8000,https://hypn0.ru +DJANGO_CSRF_TRUSTED_ORIGINS=http://127.0.0.1:8000,http://localhost:8000,https://hypn0.xyz + +# ====== Получить код для Django: +# poetry run python hypn0/manage.py shell +# Затем выполнить две строчки кода: +# from django.core.management.utils import get_random_secret_key +# print(get_random_secret_key()) +DJANGO_SECRET_KEY=CHANGE_ME # Имя файла SQLite-базы. Путь всегда собирается через `BASE_DIR.parent / 'database'`. DJANGO_SQLITE_NAME=hypn0-db.sqlite3 diff --git a/.gitea/workflows/docker-publish.yaml b/.gitea/workflows/docker-publish.yaml new file mode 100644 index 0000000..ed7754d --- /dev/null +++ b/.gitea/workflows/docker-publish.yaml @@ -0,0 +1,74 @@ +name: HYPN0 Build and Push Docker Image +run-name: HYPN0 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: ${{ github.actor }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + # Извлечение метаданных (тегов и лейблов) для 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=registry + # cache-from: type=gha + cache-to: type=registry,mode=max + # cache-to: type=gha,mode=max + timeout: 1800 # Увеличено до 30 минут на всю сборку diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b10b36e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,99 @@ +# ================================================= +# STAGE 1: Builder - Установка зависимостей +# ================================================= +FROM python:3.14-slim AS builder + +# Устанавливаем переменные окружения +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV PIP_DEFAULT_TIMEOUT=100 + +# ENV POETRY_VERSION=2.1.1 +# ENV POETRY_HOME="/opt/poetry" +# ENV POETRY_NO_INTERACTION=1 +# ENV POETRY_VIRTUALENVS_IN_PROJECT=true + +# Устанавливаем Poetry +RUN pip install --no-cache-dir --default-timeout=100 --retries 10 poetry poetry-plugin-export + +# Создаем рабочую директорию +WORKDIR /app + +# Копируем только файлы зависимостей для кэширования этого слоя +COPY pyproject.toml poetry.lock /app/ + +# Экспортируем lock-файл в requirements.txt и ставим зависимости через pip. +# Это обычно быстрее и проще для Docker, чем полноценная установка через Poetry. +RUN poetry export --format requirements.txt --without-hashes --with dev --output /tmp/requirements.txt \ + && pip install --no-cache-dir --default-timeout=100 --retries 10 -r /tmp/requirements.txt + + +# ================================================= +# STAGE 2: Final - Создание чистого и безопасного образа +# ================================================= +FROM python:3.14-slim AS stage-final + +# Устанавливаем переменные окружения +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV DJANGO_SETTINGS_MODULE=hypn0.settings +ENV PYTHONPATH="/home/app/web/hypn0" +ENV HOME="/home/app" + + +# Создаем пользователя без прав root для безопасности +# RUN addgroup --system app && adduser --system --ingroup app app + +# Создаем рабочую директорию +WORKDIR /home/app/web + +# Копируем установленные Python-пакеты из builder-стадии +# Каталог /usr/local/bin нужен для бинарных файлов, таких как gunicorn, pytest, whitenoise и т.п. +COPY --from=builder /usr/local/bin /usr/local/bin +#Каталог /usr/local/lib/python3.14/site-packages нужен для обычных Python-пакетов и батареек +COPY --from=builder /usr/local/lib/python3.14/site-packages /usr/local/lib/python3.14/site-packages + +# Копируем исходный код проекта и устанавливаем правильного владельца +# ИЗМЕНЕНИЕ: app:app -> 1000:1000 +COPY --chown=1000:1000 . . + +# 1. Создаём директорию для конфигов nginx и даём права пользователю app +# Это выполняется ещё от root, поэтому проблем с permissions не будет. +# 2. Создаём директорию для собранной статики и даём права пользователю app. +# `STATIC_ROOT` в settings.py живёт внутри `public`. +# 3. Создаём директорию для ошибок (404, 500) и даём права пользователю app +# 4. Создаём директорию для БД и даём права пользователю app +# Это важно когда БД монтируется как том с хоста +RUN mkdir -p /home/app \ + /nginx_configs_host/nginx \ + /home/app/web/public/staticfiles \ + /home/app/web/public/media/_error \ + /home/app/web/database && \ + chown -R 1000:1000 /nginx_configs_host /home/app/web/public /home/app/web/database + +# Переключаемся на пользователя без прав root +USER 1000 + + +# Собираем статику +# Используем dummy ключ, так как .env файла нет на этапе сборки +RUN SECRET_KEY=dummy python hypn0/manage.py collectstatic --noinput --clear + +# Открываем порт +EXPOSE 8000 + +# Проверка здоровья контейнера +# Docker будет периодически проверять, жив ли контейнер, отправляя GET запрос к главной странице. +# Параметры: +# --interval=30s - проверка каждые 30 секунд +# --timeout=3s - ожидаем ответ максимум 3 секунды +# --start-period=10s - даем контейнеру 10 секунд на запуск перед первой проверкой +# --retries=3 - объявляем контейнер unhealthy после 3 неудачных попыток +HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/').read()" || exit 1 + +# Переходим в директорию с manage.py для корректного запуска gunicorn +WORKDIR /home/app/web/hypn0 + +# Команда запуска (два воркера для лучшей производительности, можно увеличить до число ядер на хосте) +CMD ["python", "-m", "gunicorn", "--workers", "2", "--bind", "0.0.0.0:8000", "hypn0.wsgi:application"] diff --git a/config/nginx/hypn0-app--external-nginx.conf b/config/nginx/hypn0-app--external-nginx.conf new file mode 100644 index 0000000..ed277a6 --- /dev/null +++ b/config/nginx/hypn0-app--external-nginx.conf @@ -0,0 +1,147 @@ +# config/nginx/hypn0-app--external-nginx.conf +# ============================================================================== +# ЭТАЛОННЫЙ КОНФИГУРАЦИОННЫЙ ФАЙЛ NGINX (Reverse Proxy для Docker) +# ============================================================================== +# +# ВНИМАНИЕ: +# Этот файл является шаблоном. При первом деплое он копируется в `/home/user/app/hypn0-site/config/nginx/hypn0-app--external-nginx.conf`, +# а затем (уже руками) через силинк в `/etc/nginx/sites-available/` и активируется. +# При последующих деплоях он НЕ ПЕРЕЗАПИСЫВАЕТСЯ автоматически, чтобы не затереть SSL-сертификаты и ручные правки. +# +# Если вы изменили этот файл в репозитории и хотите применить изменения на проде: +# вам нужно обновить файл в `/home/user/app/hypn0-site/config/nginx/hypn0-app--external-nginx.conf` вручную (diff + copy). +# +# Так же (рядом) будет создан образец этого файла `nginx_hypn0.conf.example`, который будет обновляться при деплоях +# из репозитория, чтобы вы могли видеть, что изменилось и при необходимости перенести эти изменения на прод. +# +# Предполагаемая структура на сервере: +# /home/user/app/hypn0-site/ +# ├── docker-compose.yml +# ├── .env +# ├── media/ <-- Сюда Nginx смотрит напрямую (Docker volume) +# └── ... + +# 1. Описываем, где живет наш Django в Docker +upstream hypn0-django { + # Мы пробрасываем порт 8050 из контейнера наружу (в docker-compose.yml имя сервиса 'web', контейнер 'hypn0-backend') + server 127.0.0.1:8042; + keepalive_requests 200; +} + +# 2. Конфигурируем сервер +server { + server_name hypn0.xyz; # Основное доменное имя + + # Слушаем 80 порт (Certbot потом добавит сюда редирект на 443 и настройки SSL) + listen 80; + listen [::]:80; + + charset utf-8; + client_max_body_size 10M; # Разрешаем загрузку не слишком больших картинок + + # Логи (пути могут отличаться в зависимости от настроек сервера, здесь стандартные для Ubuntu) + access_log /var/log/nginx/hypn0.access.log; + error_log /var/log/nginx/hypn0.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/hypn0-site -> ваш реальный путь + location /media/ { + alias /home/user/app/hypn0-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/hypn0-site/media/_error; internal; } + location = /502.html { root /home/user/app/hypn0-site/media/_error; internal; } + location = /503.html { root /home/user/app/hypn0-site/media/_error; internal; } + location = /504.html { root /home/user/app/hypn0-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/hypn0-site/media/_error; internal; } + location = /401.html { root /home/user/app/hypn0-site/media/_error; internal; } + location = /403.html { root /home/user/app/hypn0-site/media/_error; internal; } + location = /404.html { root /home/user/app/hypn0-site/media/_error; internal; } + location = /413.html { root /home/user/app/hypn0-site/media/_error; internal; } + location = /429.html { root /home/user/app/hypn0-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/hypn0-site/media/_error; internal; } + + # --- ВСЁ ОСТАЛЬНОЕ (Django + WhiteNoise) --- + # Статика (/static/), robots.txt, favicon.ico и сам сайт обрабатываются внутри контейнера. + # Nginx просто прокидывает запрос внутрь. + location / { + proxy_pass http://hypn0-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.hypn0.ru; +# listen 80; +# return 301 $scheme://hypn0.ru$request_uri; # Всегда редиректим на основной домен +# } \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..59de1bd --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,69 @@ +# ============================================================================== +# Docker Compose для РАЗРАБОТКИ (Local Development) +# Этот файл содержит настройки для локальной работы (live reload, debug). +# Запуск: docker compose -f docker-compose.local up --build +# ============================================================================== + +services: + web: + # Имя контейнера для удобства + container_name: hypn0-backend-dev + + # Сборка из текущей директории + build: . + + # Проброс портов (чтобы сайт был доступен на localhost:8042) + ports: + - "8042:8000" + + # 1. КОМАНДА ЗАПУСКА (Dev режим) + # 1.A. Запускаем миграции, чтобы база была актуальной. + # 1.B. Запускаем кастомную команду rehash, чтобы обновить id-хэши для SVG-генераций в соответствии + # с HASHIDS_SALT из текущего .env + # 1.C. Запускаем Gunicorn с 1 воркером и включаем --reload для авто-перезагрузки при изменении кода. + # Уменьшаем число воркеров до 1 (ресурсы dev-машины можно не экономит, но одного достаточно). + # Убираем collectstatic (в dev Django сам может отдавать статику или она нам не так важна сжатой) + command: > + sh -c "python manage.py migrate --noinput && + python manage.py rehash && + python -m gunicorn --workers 1 --bind 0.0.0.0:8000 --reload hypn0.wsgi:application" + + # 2. МОНТИРОВАНИЕ КОДА (Live Reload) + # Подключаем локальные папки внутрь контейнера, чтобы Gunicorn видел изменения без пересборки образа. + volumes: + # Монтируем основной код проекта. + # Так как web, templates и manage.py лежат внутри hypn0/, одного этого маунта достаточно. + - ./hypn0:/home/app/web/hypn0 + + # Монтируем всю папку public (Static + Media) + # Это нужно, чтобы: + # 1. Изменения в CSS/JS (public/static) сразу были видны (Live Reload). + # 2. Загруженные картинки (public/media) сохранялись на диске. + - ./public:/home/app/web/public + + # Монтируем базу данных (чтобы данные сохранялись при пересоздании контейнера) + # Используем ту же папку database, что и на проде, для единообразия. + # ВАЖНО: Django ищет базу в BASE_DIR.parent / 'database/db.sqlite3' + # В контейнере BASE_DIR=/home/app/web/hypn0, значит путь к базе: /home/app/web/database/db.sqlite3 + - ./database:/home/app/web/database + + # 3. ПЕРЕМЕННЫЕ ОКРУЖЕНИЯ + env_file: + # файл с переменными окружения для разработки + - .env + environment: + # на всякий случай, принудительно включаем DEBUG и DEBUG-уровень логов (вдруг в .env что-то не так) + # - DJANGO_DEBUG=False + - DJANGO_DEBUG=True + - DJANGO_LOG_LEVEL=DEBUG + # В dev нам не нужно ограничивать буферизацию так строго, но не помешает. + + # 4. РЕСУРСЫ (Без лимитов для разработки) + # Удаляем секцию ограничений, чтобы локально использовать все доступные ресурсы хоста. + # deploy: + # resources: + # limits: + # cpus: ... + # memory: ... + # mem_limit: ... + diff --git a/hypn0/hypn0/urls.py b/hypn0/hypn0/urls.py index 0d49b32..999fd8e 100644 --- a/hypn0/hypn0/urls.py +++ b/hypn0/hypn0/urls.py @@ -34,6 +34,7 @@ if settings.DEBUG: import mimetypes import debug_toolbar from django.views.static import serve + from django.contrib.staticfiles.urls import staticfiles_urlpatterns def _serve_public_root_file(request, path): """Отдаёт файлы из корня `public` в dev-режиме в utf-8.""" @@ -68,5 +69,4 @@ if settings.DEBUG: urlpatterns = [path('__debug__/', include(debug_toolbar.urls)), ] + urlpatterns urlpatterns = [*PUBLIC_ROOT_URLPATTERNS, *urlpatterns] - urlpatterns += static(settings.STATIC_URL, document_root=settings.PUBLIC_DIR.joinpath('static')) - urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += staticfiles_urlpatterns()