This commit is contained in:
@@ -67,9 +67,7 @@ jobs:
|
||||
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
|
||||
# ДОБАВЛЕНО для оптимизации сборки (кэш слоёв в реестре):
|
||||
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
|
||||
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
|
||||
timeout: 1800 # Увеличено до 30 минут на всю сборку
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
# ==============================================================================
|
||||
# Docker Compose для PRODUCTION
|
||||
# Этот файл запускается на боевом сервере.
|
||||
# Вариант 1 (если переименовали в docker-compose.yml): docker compose up -d
|
||||
# Вариант 2 (если оставили имя): docker compose -f docker-compose.prod-test.yml up -d
|
||||
# ==============================================================================
|
||||
|
||||
# В новой версии Docker не нужно
|
||||
# version: '3.8'
|
||||
|
||||
services:
|
||||
# --- СБОРКА ТАИЛВИНД CSS (Фронтенд-компилятор) ---
|
||||
# Запускается один раз при старте, собирает свежий tailwind.min.css и завершает работу
|
||||
tailwind-builder:
|
||||
image: node:22-slim
|
||||
container_name: hypn0-tailwind-builder
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- .:/app
|
||||
command: bash ./scripts/build-tailwind.sh
|
||||
restart: "no"
|
||||
|
||||
# --- ОCНОВНОЙ СЕРВИС: DJANGO + GUNICORN + WHITENOISE ---
|
||||
web:
|
||||
# Имя контейнера
|
||||
container_name: hypn0-backend
|
||||
|
||||
# Ждём успешного завершения компиляции стилей перед запуском бэкенда
|
||||
depends_on:
|
||||
tailwind-builder:
|
||||
condition: service_completed_successfully
|
||||
|
||||
# 1. ОБРАЗ
|
||||
# В продакшене мы используем готовый, собранный образ из реестра (Gitea)
|
||||
image: git.cube2.ru/erjemin/2026-hypn0-site:latest
|
||||
# Если образа в gitae нет, то перенести весь код в прод и можно собирать локально:
|
||||
# build: .
|
||||
|
||||
restart: always
|
||||
|
||||
# 2. Метки для Watchtower (авто-обновление)
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.scope=hypn0-scope"
|
||||
|
||||
# 3. КОМАНДА ЗАПУСКА (Замена entrypoint.sh)
|
||||
# Выполняем цепочку команд внутри контейнера при запуске:
|
||||
# a. Миграции
|
||||
# b. Collectstatic
|
||||
# c. рехеш-id
|
||||
# d. Создаем папку nginx в примонтированном томе конфигов (если нет)
|
||||
# e. Копирование конфига Nginx с авто-заменой путей через sed (замену реального пути на хосте получаем
|
||||
# через переменную окружения HOST_PROJECT_PATH)
|
||||
# f. Инициализация боевого конфига (если нет)
|
||||
# g. Создаем папку для ошибок и копируем туда HTML error pages и их ассеты (там их увидит Nginx хоста)
|
||||
# — иконки, SVG-иллюстрации и under_reconstruction тоже должны лежать рядом, чтобы Nginx мог их отдать
|
||||
# h. Запуск Gunicorn
|
||||
command: >
|
||||
sh -c "python manage.py migrate --noinput &&
|
||||
python manage.py collectstatic --noinput --clear &&
|
||||
python manage.py rehash &&
|
||||
mkdir -p /nginx_configs_host/nginx &&
|
||||
sed \"s|/home/user/app/hypn0-site|${HOST_PROJECT_PATH:-/home/default_user/projects/hypn0-site}|g\" /home/app/web/config/nginx/hypn0-app--external-nginx.conf > /nginx_configs_host/nginx/nginx_hypn0.conf.example &&
|
||||
if [ ! -f /nginx_configs_host/nginx/hypn0-app--external-nginx.conf ]; then
|
||||
cp /nginx_configs_host/nginx/nginx_hypn0.conf.example /nginx_configs_host/nginx/hypn0-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" &&
|
||||
for code in 400 401 403 404 413 429 500 502 503 504; do
|
||||
cp /home/app/web/hypn0/templates/_error/$${code}.html "$$ERROR_DIR/$${code}.html";
|
||||
done &&
|
||||
cp /home/app/web/hypn0/templates/_error/under_reconstruction.html "$$ERROR_DIR/under_reconstruction.html" &&
|
||||
cp /home/app/web/public/static/img/favicon.svg "$$ERROR_DIR/favicon.svg" &&
|
||||
cp /home/app/web/public/static/img/favicon.ico "$$ERROR_DIR/favicon.ico" &&
|
||||
cp /home/app/web/public/static/img/logo-hypn0.svg "$$ERROR_DIR/logo-hypn0.svg" &&
|
||||
cp /home/app/web/public/static/img/thinking.svg "$$ERROR_DIR/thinking.svg" &&
|
||||
echo '*/30 * * * * cd '${HOST_PROJECT_PATH:-/home/default_user/projects/hypn0-site}' && docker compose exec -T web python manage.py rescore >> /var/log/hypn0-rescore.log 2>&1' > /nginx_configs_host/crontab.example &&
|
||||
echo '' &&
|
||||
echo '========================================================================' &&
|
||||
echo ' [HYPN0 CRON NOTICE] Слава Гипножабе! Не забудь настроить cron на хосте:' &&
|
||||
echo ' Добавь строку в `crontab -e` (или см. config/crontab.example):' &&
|
||||
echo ' */30 * * * * cd '${HOST_PROJECT_PATH:-/home/default_user/projects/hypn0-site}' && docker compose exec -T web python manage.py rescore' &&
|
||||
echo '========================================================================' &&
|
||||
echo '' &&
|
||||
python -m gunicorn --workers 2 --bind 0.0.0.0:8000 hypn0.wsgi:application"
|
||||
|
||||
# 4. Проброс портов (Внешний Nginx -> localhost:8042)
|
||||
ports:
|
||||
# Слушаем только на localhost хоста, чтобы закрыть прямой доступ из интернета к Gunicorn
|
||||
- "127.0.0.1:8042: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. Пользователь и права
|
||||
# На первом старте контейнеру нужны права на инициализацию bind-mount'ов
|
||||
# (media/config/database) на хосте, иначе `mkdir` для `media/_error` падает с Permission denied.
|
||||
# После первичной инициализации можно будет отдельно ужесточить права на уровне хоста.
|
||||
user: "0:0"
|
||||
|
||||
# Когда нужна отладка процессов внутри контейнера, можно временно раскомментировать эту строку и запустить контейнер с правами root.
|
||||
# cap_add:
|
||||
# - SYS_PTRACE
|
||||
|
||||
# 7. Переменные окружения
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- DJANGO_SETTINGS_MODULE=hypn0.settings
|
||||
- PYTHONUNBUFFERED=1
|
||||
# Передаем переменную с путем на хосте внутрь контейнера, чтобы sed мог её использовать
|
||||
- HOST_PROJECT_PATH=${HOST_PROJECT_PATH:-/home/default_user/projects/hypn0-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: hypn0_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=hypn0-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"
|
||||
@@ -234,6 +234,9 @@ else:
|
||||
STORAGES['staticfiles'] = {
|
||||
'BACKEND': 'whitenoise.storage.CompressedManifestStaticFilesStorage',
|
||||
}
|
||||
# Предотвращает 500 ошибку, если какой-то статический файл отсутствует в манифесте
|
||||
WHITENOISE_MANIFEST_STRICT = env.bool('DJANGO_WHITENOISE_MANIFEST_STRICT', default=False)
|
||||
|
||||
# Конфигурация WhiteNoise для обслуживания статических файлов и файлов из /public (например,
|
||||
# robots.txt, favicon.ico и т.п.)
|
||||
WHITENOISE_ROOT = PUBLIC_DIR
|
||||
|
||||
+11
-1
@@ -16,8 +16,9 @@ Including another URLconf
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.contrib.sitemaps.views import sitemap
|
||||
from django.urls import include, path
|
||||
from django.urls import include, path, re_path
|
||||
from django.conf.urls.static import static
|
||||
from django.views.static import serve
|
||||
|
||||
from hypn0_site.sitemaps import sitemaps
|
||||
from . import settings
|
||||
@@ -75,3 +76,12 @@ if settings.DEBUG:
|
||||
urlpatterns = [path('__debug__/', include(debug_toolbar.urls)), ] + urlpatterns
|
||||
urlpatterns = [*PUBLIC_ROOT_URLPATTERNS, *urlpatterns]
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
|
||||
# ==============================================================================
|
||||
# РАЗДАЧА МЕДИА-ФАЙЛОВ (/media/...)
|
||||
# ВНИМАНИЕ: В боевом продакшене отдачей /media/ занимается внешний Nginx напрямую.
|
||||
# Этот маршрут нужен для локального тестирования (в dev и в standalone prod-тестах без Nginx).
|
||||
# ==============================================================================
|
||||
# urlpatterns += [
|
||||
# re_path(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
|
||||
# ]
|
||||
|
||||
Reference in New Issue
Block a user