From 8fe641c8f46f0c907de7e9e62a114f69a7ff11d0 Mon Sep 17 00:00:00 2001 From: erjemin Date: Mon, 18 May 2026 19:18:03 +0300 Subject: [PATCH] =?UTF-8?q?add:=20=D0=BF=D1=80=D0=B5=D0=B4-=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B4=20=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=B9=D0=BD=D0=B5?= =?UTF-8?q?=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.sample | 10 ++ .gitea/workflows/docker-publish.yaml | 72 +++++++++ .../nginx/oknardia-app--external-nginx.conf | 150 ++++++++++++++++++ config/oknardia.conf | 110 ------------- oknardia/oknardia/settings.py | 32 +++- oknardia/oknardia/urls.py | 25 ++- 6 files changed, 283 insertions(+), 116 deletions(-) create mode 100644 .gitea/workflows/docker-publish.yaml create mode 100644 config/nginx/oknardia-app--external-nginx.conf delete mode 100644 config/oknardia.conf diff --git a/.env.sample b/.env.sample index a5d080d..759f3a3 100644 --- a/.env.sample +++ b/.env.sample @@ -117,6 +117,16 @@ LOG_LEVEL=INFO # CELERY_BROKER_URL=redis://localhost:6379/0 # CELERY_RESULT_BACKEND=redis://localhost:6379/0 +# ============================================================================ +# DOCKER: LOCAL PRODUCTION TESTING +# ============================================================================ + +# Разрешить Django обслуживать медиа-файлы через Python (только для локального тестирования) +# ВАЖНО: В настоящем production медиа и статику обслуживает Nginx, а не Django! +# Используется ТОЛЬКО в docker-compose.local-prod.yml для локального тестирования production конфигурации. +# На production сервере НЕ устанавливайте это значение в True! +ALLOW_MEDIA_SERVE=False + # ============================================================================ # ИНСТРУКЦИЯ ПО ИСПОЛЬЗОВАНИЮ # ============================================================================ diff --git a/.gitea/workflows/docker-publish.yaml b/.gitea/workflows/docker-publish.yaml new file mode 100644 index 0000000..fecc053 --- /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: ${{ 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 (для скорости + все равно сервер на x86). + platforms: linux/amd64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + # ДОБАВЛЕНО для медленного интернета и оптимизации сборки: + cache-from: type=gha + cache-to: type=gha,mode=max + timeout: 1800 # Увеличено до 30 минут на всю сборку diff --git a/config/nginx/oknardia-app--external-nginx.conf b/config/nginx/oknardia-app--external-nginx.conf new file mode 100644 index 0000000..d0dda07 --- /dev/null +++ b/config/nginx/oknardia-app--external-nginx.conf @@ -0,0 +1,150 @@ +# config/nginx/oknardia-app--external-nginx.conf +# ============================================================================== +# ЭТАЛОННЫЙ КОНФИГУРАЦИОННЫЙ ФАЙЛ NGINX (Reverse Proxy для Docker) +# ============================================================================== +# +# ВНИМАНИЕ: +# Этот файл является шаблоном. При первом деплое он копируется +# в `/home/user/path-to-oknardia-app/config/nginx/oknardia-app--external-nginx.conf`, +# а затем (уже на хосте, руками) через силинк в `/etc/nginx/sites-available/` и активируется. +# При последующих деплоях `oknardia-app--external-nginx.conf` НЕ ПЕРЕЗАПИСЫВАЕТСЯ, +# чтобы не затереть SSL-сертификаты и ручные правки. +# +# Если вы изменили этот файл в репозитории и хотите применить изменения на проде: +# вам нужно обновить файл в `/home/user/path-to-oknardia-app/config/nginx/oknardia-app--external-nginx.conf` вручную (diff + copy). +# +# Так же (рядом) будет создан образец этого файла `nginx_oknardia.conf.example`, который будет обновляться при деплоях +# из репозитория, чтобы вы могли видеть, что изменилось и при необходимости перенести эти изменения на прод. +# +# Предполагаемая структура на сервере: +# /home/user/path-to-oknardia-app/ +# ├── docker-compose.yml +# ├── .env +# ├── media/ <-- Сюда Nginx смотрит напрямую (Docker volume) +# └── ... + +# 1. Описываем, где живет наш Django в Docker +upstream oknardia-django { + # Мы пробрасываем порт 8050 из контейнера наружу (в docker-compose.yml имя сервиса 'web', контейнер 'oknardia-backend') + server 127.0.0.1:8060; + keepalive_requests 200; +} + +# 2. Конфигурируем сервер +server { + server_name tmp.oknardia.ru; # Основное доменное имя + + # Слушаем 80 порт (Certbot потом добавит сюда редирект на 443 и настройки SSL) + listen 80; + listen [::]:80; + + charset utf-8; + client_max_body_size 10M; # Разрешаем загрузку не слишком больших картинок + + # Логи (пути могут отличаться в зависимости от настроек сервера, здесь стандартные для Ubuntu) + access_log /var/log/nginx/oknardia.access.log; + error_log /var/log/nginx/oknardia.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/path-to-oknardia-app -> ваш реальный путь + location /media/ { + alias /home/user/path-to-oknardia-app/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/path-to-oknardia-app/media/_error; internal; } + location = /502.html { root /home/user/path-to-oknardia-app/media/_error; internal; } + location = /503.html { root /home/user/path-to-oknardia-app/media/_error; internal; } + location = /504.html { root /home/user/path-to-oknardia-app/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/path-to-oknardia-app/media/_error; internal; } + location = /401.html { root /home/user/path-to-oknardia-app/media/_error; internal; } + location = /403.html { root /home/user/path-to-oknardia-app/media/_error; internal; } + location = /404.html { root /home/user/path-to-oknardia-app/media/_error; internal; } + location = /413.html { root /home/user/path-to-oknardia-app/media/_error; internal; } + location = /429.html { root /home/user/path-to-oknardia-app/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/path-to-oknardia-app/media/_error; internal; } + + # --- ВСЁ ОСТАЛЬНОЕ (Django + WhiteNoise) --- + # Статика (/static/), robots.txt, favicon.ico и сам сайт обрабатываются внутри контейнера. + # Nginx просто прокидывает запрос внутрь. + location / { + proxy_pass http://oknardia-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.tmp.oknardia.ru; + listen 80; + return 301 $scheme://oknardia.ru$request_uri; # Всегда редиректим на основной домен +} + diff --git a/config/oknardia.conf b/config/oknardia.conf deleted file mode 100644 index 249aef1..0000000 --- a/config/oknardia.conf +++ /dev/null @@ -1,110 +0,0 @@ -# Разработка сайта OKNARDIA.RU -# == Конфикурационный файл nginx oknardia.conf - -# Описываем апстрим-потоки которые должен подключить Nginx -# Для каждого сайта надо настроить свйо поток, со своим уникальным именем. -# Если будете настраивать несколько python (django) сайтов - измените название upstream - -upstream oknardia-django { - # расположение файла Unix-сокет для взаимодействие с uwsgi - server unix:/home/web/oknardia-ru/socket/oknardia.sock; - # server ///home/web/oknardia-ru/socket/oknardia.sock; - # также можно использовать веб-сокет (порт) для взаимодействие с uwsgi. Но это медленнее - # server 127.0.0.1:8001; # для взаимодействия с uwsgi через веб-порт - keepalive_requests 200; -} - -# конфигурируем сервер -server { - server_name tmp.cube2.ru; # доменное имя сайта - listen 80; # managed by Certbot - ## listen 443 ssl http2; # managed by Certbot - ## ssl_certificate /etc/letsencrypt/live/cadpoint.ru/fullchain.pem; # managed by Certbot - ## ssl_certificate_key /etc/letsencrypt/live/cadpoint.ru/privkey.pem; # managed by Certbot - ## include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot - ## ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot - - # server_name 90.156.203.25; # доменное имя сайта - charset utf-8; # кодировка по умолчанию - access_log /home/web/oknardia-ru/logs/oknardia-access.log; # логи с доступом - error_log /home/web/oknardia-ru/logs/oknardia-error.log; # логи с ошибками - client_max_body_size 100M; # максимальный объем файла для загрузки на сайт (max upload size) - error_page 404 /404.html; - error_page 500 /500.html; - - location /media { alias /home/web/oknardia-ru/public/media; } # Расположение media-файлов Django - location /static { alias /home/web/oknardia-ru/public/static; } # Расположение static-файлов Django - - location /robots.txt { root /home/web/oknardia-ru/public; } # Расположение robots.txt - location /favicon.ico { root /home/web/oknardia-ru/public; } # Расположение favicon.ico - location /favicon.gif { root /home/web/oknardia-ru/public; } # Расположение favicon - location /favicon.png { root /home/web/oknardia-ru/public; } # Расположение favicon - location /favicon.svg { root /home/web/oknardia-ru/public; } # Расположение favicon - location /author.txt { root /home/web/oknardia-ru/public; } # Расположение author.txt - location = /404.html { - root /home/web/oknardia-ru/oknardia/templates/404.html; - internal; - } - location = /500.html { - root /home/web/oknardia-ru/oknardia/templates/500.html; - internal; - } - # location ~ \.(html|htm|ico|svg|png|gif|jpg|jpeg)$ { - location ~ \.(xml|html|htm|ico|svg|png|gif|jpg|jpeg)$ { - root /home/web/oknardia-ru/public; # Расположение статичных *.xml, *.html и *.txt - } - - location / { - uwsgi_pass oknardia-django; # upstream обрабатывающий обращений - include uwsgi_params; # конфигурационный файл uwsgi; - proxy_set_header Host $host; - - # ограничение количества запросов c одного IP-адреса с помощью модуля Limit_Req_Module - # limit_req zone=one burst=20 nodelay; - # one — имя зоны настроеной в /etc/nginx/nginx.conf (для всех сайтов сервера) в блоке http {…} - # burst — максимальный всплеск активности, можно регулировать до какого значения запросов - # в секунду может быть всплеск запросов; - # nodelay — незамедлительно, при достижении лимита подключений, выдавать код 503 - # (Service Unavailable) для этого IP - - fastcgi_keep_conn on; - uwsgi_read_timeout 1800; # некоторые запросы на Raspbery pi очень долго обрабатываются. Например, переиндексация. - uwsgi_send_timeout 200; # на всякий случай время записи в сокет - } -} - -# переадресация с www на "без" www -server { - server_name www.tmp.cube2.ru; - listen 80; - return 301 http://tmp.cube2.ru$request_uri; - - ## listen 443 ssl; # managed by Certbot - ## ssl_certificate /etc/letsencrypt/live/cadpoint.ru/fullchain.pem; # managed by Certbot - ## ssl_certificate_key /etc/letsencrypt/live/cadpoint.ru/privkey.pem; # managed by Certbot - ## include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot - ## ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot -} - -# переадресация с http на https -##server { -## if ($host = oknardia.ru) { -## return 301 https://$host$request_uri; -## } # managed by Certbot -## -## listen 80; -## server_name cadpoint.ru; -## return 404; # managed by Certbot -##} - -# переадресация с http на https для www -##server { -## if ($host = www.cadpoint.ru) { -## return 301 https://cadpoint.ru$request_uri; -## } # managed by Certbot -## -## listen 80; -## server_name www.cadpoint.ru; -## return 404; # managed by Certbot -##} - diff --git a/oknardia/oknardia/settings.py b/oknardia/oknardia/settings.py index 26403b8..492ee32 100644 --- a/oknardia/oknardia/settings.py +++ b/oknardia/oknardia/settings.py @@ -344,15 +344,25 @@ if DEBUG: pass else: - # В prod: WhiteNoise + CompressedManifestStaticFilesStorage для оптимизации. + # В prod: WhiteNoise + CompressedStaticFilesStorage для оптимизации. # Статика собирается с хешем в имени и кэшируется. + # + # ВАЖНО: Для production нужна полная настройка Nginx: + # - Nginx обслуживает статику (/static/) прямо из /home/app/public/static_collected/ + # - Nginx обслуживает медиа (/media/) прямо из /home/app/public/media/ + # - Nginx проксирует остальное на Gunicorn (:8000) + # + # Для локального тестирования production конфига этот файл симулирует Nginx. # 1. Добавляем WhiteNoise в начало MIDDLEWARE (после SecurityMiddleware) для отдачи статики MIDDLEWARE.insert(1, 'whitenoise.middleware.WhiteNoiseMiddleware') # 2. Переводим staticfiles на WhiteNoise со сжатием + # ВАЖНО: используем CompressedStaticFilesStorage вместо CompressedManifestStaticFilesStorage, + # потому что Manifest не справляется с relative paths в CSS (например, jQuery UI). + # CompressedStaticFilesStorage сжимает файлы без manifest, что работает надежнее. STORAGES['staticfiles'] = { - 'BACKEND': 'whitenoise.storage.CompressedManifestStaticFilesStorage', # noqa: F821 + 'BACKEND': 'whitenoise.storage.CompressedStaticFilesStorage', # noqa: F821 } # 3. WhiteNoise конфиг: обслуживание корневых файлов из public/ (robots.txt, favicon.*, sitemap.xml и т.д.) @@ -364,7 +374,21 @@ else: '.woff': 'font/woff', '.woff2': 'font/woff2', } + # 5. Конфигурация WhiteNoise для обслуживания статических файлов и файлов из /public (например, + # robots.txt, favicon.ico и т.п.) + # WHITENOISE_ROOT = PUBLIC_ROOT + + # 6. Кэширование неизменяемых файлов (с хешем в имени) на 1 год в браузере + # ВАЖНО: лямбда должна принимает ДВА аргумента: path и url (как требует WhiteNoise) + WHITENOISE_IMMUTABLE_FILE_TEST = lambda path, url: 'CACHE' in path + + # 7. ЛОКАЛЬНЫЙ ТЕСТ: для отдачи медиа в docker-compose.local-prod.yml + # В реальном production это обслуживает Nginx! Никогда не используй в production! + # Добавляем StaticFilesHandler который обслуживает и медиа и статику + if env.bool('ALLOW_MEDIA_SERVE', default=False): + # Для локального тестирования добавляем обслуживание медиа через Django + # ВАЖНО: это очень медленно и небезопасно для production! + from django.conf.urls.static import static + # Будет добавлено в urls.py при импорте: urlpatterns += static(MEDIA_URL, document_root=MEDIA_ROOT) - # 5. Кэширование неизменяемых файлов (с хешем в имени) на 1 год в браузере - WHITENOISE_IMMUTABLE_FILE_TEST = lambda path: 'CACHE' in path diff --git a/oknardia/oknardia/urls.py b/oknardia/oknardia/urls.py index 08b6318..da6631c 100644 --- a/oknardia/oknardia/urls.py +++ b/oknardia/oknardia/urls.py @@ -17,6 +17,12 @@ from django.contrib import admin from django.urls import include, path, re_path from django.conf.urls.static import static +from pathlib import Path +import environ +# Инициализируем env +env = environ.Env() +PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent +environ.Env.read_env(str(PROJECT_ROOT / '.env')) from oknardia.settings import * from web import views, autocomplete_addr, user_manager, blog, diagrams, report1, report2, catalog, prices, service, \ catalog_profiles, catalog_series, catalog_openings, catalog_companies @@ -105,9 +111,24 @@ urlpatterns = [ ] +# Для локального тестирования production конфига: отдача медиа через Django +# В реальном production медиа обслуживает Nginx! +import os +if DEBUG or env.bool('ALLOW_MEDIA_SERVE', default=False): + from django.views.static import serve as serve_static + # Проверяем что директория медиа существует + if os.path.isdir(MEDIA_ROOT): + # Добавляем URL pattern для отдачи медиа файлов + urlpatterns += [ + re_path( + r'^media/(?P.*)$', + serve_static, + {'document_root': MEDIA_ROOT}, + name='media' + ), + ] + if DEBUG: - # Медиа-файлы - urlpatterns += static(MEDIA_URL, document_root=MEDIA_ROOT) # --- страничка для тестирования верстки текста в блоге urlpatterns += [re_path(r'^blog/tmp[/*]$', service.tmp),] # ___ ____ _ _____ _ _ _____ _