merge: bring 2026-containerization into main

This commit is contained in:
2026-04-15 18:06:49 +03:00
17 changed files with 881 additions and 1855 deletions

46
.dockerignore Normal file
View File

@@ -0,0 +1,46 @@
# Исключаем мусор и локальные артефакты, чтобы 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/
Dockerfile
docker-compose*.yml

View File

@@ -20,3 +20,16 @@ DJANGO_EMAIL_FROM=you@email.com
# URL для доступа к админке Django (можно сменить для безопасности, чтобы боты не могли её найти) # URL для доступа к админке Django (можно сменить для безопасности, чтобы боты не могли её найти)
ADMIN_URL=admin/ ADMIN_URL=admin/
# Системные пути на хосте (ТОЛЬКО ДЛЯ ПРОДАКШЕНА)
# Используется скриптом для генерации корректного Nginx конфига (alias к медиа-файлам).
# На локальной машине разработчика (Dev) эта переменная игнорируется.
# В ПРОДАКШЕНЕ: Укажите полный путь к папке проекта на сервере
HOST_PROJECT_PATH=/home/username/projects
# Настройки доступа к пакетам в репозитории, чтобы wathtower мог проверять их свежесть и скачивать для обновления.
# Получить эти данные можно в настройках вашего репозитория, например:
# для GitHub: в разделе "Developer settings" -> "Personal access tokens";
# для Gitea: в разделе "Settings / Настройки" -> "Actions / Действия" -> "Secrets / Секреты".
REPO_USER=[login]
REPO_PASS=[token]

View File

@@ -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 (для скорости)
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 минут на всю сборку

2
.gitignore vendored
View File

@@ -331,3 +331,5 @@ cython_debug/
*.sql *.sql
my_secret*.py my_secret*.py
.github/ .github/
# для картинок
_4image_and_design/

90
Dockerfile Normal file
View File

@@ -0,0 +1,90 @@
# =================================================
# STAGE 1: Builder - Установка зависимостей
# =================================================
FROM python:3.12-slim AS builder
# Устанавливаем переменные окружения
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PIP_DEFAULT_TIMEOUT=100
# Устанавливаем 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.12-slim AS stage-final
# Устанавливаем переменные окружения
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV DJANGO_SETTINGS_MODULE=cadpoint.settings
# Создаем пользователя без прав root для безопасности
# RUN addgroup --system app && adduser --system --ingroup app app
# Создаем рабочую директорию
WORKDIR /home/app/web
# Копируем установленные Python-пакеты из builder-стадии
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
# Копируем исходный код проекта и устанавливаем правильного владельца
# ИЗМЕНЕНИЕ: app:app -> 1000:1000
COPY --chown=1000:1000 . .
# Создаём директорию для конфигов nginx и даём права пользователю app
# Это выполняется ещё от root, поэтому проблем с permissions не будет.
RUN mkdir -p /nginx_configs_host/nginx && chown -R 1000:1000 /nginx_configs_host
# Создаём директорию для собранной статики и даём права пользователю app.
# `STATIC_ROOT` в settings.py живёт внутри `public`.
RUN mkdir -p /home/app/web/public/staticfiles && chown -R 1000:1000 /home/app/web/public
# Создаём директорию для ошибок (404, 500) и даём права пользователю app
RUN mkdir -p /home/app/web/public/media/_error && chown -R 1000:1000 /home/app/web/public/media
# Создаём директорию для БД и даём права пользователю app
# Это важно когда БД монтируется как том с хоста
RUN mkdir -p /home/app/web/database && chown -R 1000:1000 /home/app/web/database
# Переключаемся на пользователя без прав root
USER 1000
# Собираем статику
# Используем dummy ключ, так как .env файла нет на этапе сборки
RUN SECRET_KEY=dummy python cadpoint/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/cadpoint
# Команда запуска (два воркера для лучшей производительности, можно увеличить до число ядер на хосте)
CMD ["python", "-m", "gunicorn", "--workers", "2", "--bind", "0.0.0.0:8000", "cadpoint.wsgi:application"]

233
README.md
View File

@@ -1,91 +1,188 @@
# Сайт CADpoint.ru # CADpoint.ru
Сайт с новостями (блог о 3D-печать и Систем Автоматизированного Проектирования) на Django со Сайт CADpoint.ru — это Django-проект, новостной сайт (блог о 3D-печать и Систем Автоматизированного Проектирования) на Django который сейчас живёт в Docker. Развернут по адресу [cadpoint.ru](https://cadpoint.ru).
встроенными свистелками-перделками:
* медиа-библиотека (filer);
* HTML-редактор на обычной textarea в админке;
* типограф [etpgrf](https://typograph.cube2.ru/);
* теги новостей (taggit).
[Инструкция по развертыванию на хостинге DreamHost.com](deploy_to_dreamhost.md) Кратко о схеме:
Для локальной и продовой настройки используй файл `.env` в корне проекта. - сам Django запускается в контейнере `cadpoint-backend`;
Шаблон для него лежит в `.env.sample`. - локально используется `docker-compose.local.yml`;
- на проде используется `docker-compose.prod.yml`;
- внешний `nginx` стоит на хосте и проксирует запросы в контейнер;
- конфигурация и секреты лежат в `.env`;
- медиа, статика и SQLite-база живут в проектных каталогах и монтируются в контейнер.
Набор базовых переменных: ## Что есть в проекте
* `DJANGO_SECRET_KEY` - `cadpoint/` — Django-проект, приложения, шаблоны, management commands.
* `DJANGO_DEBUG` - `public/static/` — исходная статика проекта.
* `DJANGO_ALLOWED_HOSTS` - `public/media/` — загруженные файлы и служебные error-assets.
* `DJANGO_ADMINS` - `database/` — SQLite-база и дампы.
* `DJANGO_CSRF_TRUSTED_ORIGINS` - `config/nginx/` — конфиг внешнего `nginx` для прод-хоста.
* `DJANGO_INTERNAL_IPS` - `frontend-assembly/` — сборка CodeMirror 6 для админки.
* `DJANGO_SQLITE_NAME` - `Dockerfile` — финальный образ приложения.
* `ADMIN_URL` - `docker-compose.local.yml` — локальный запуск.
* `DJANGO_EMAIL_HOST` - `docker-compose.prod.yml` — prod-запуск.
* `DJANGO_EMAIL_PORT`
* `DJANGO_EMAIL_HOST_USER`
* `DJANGO_EMAIL_HOST_PASSWORD`
* `DJANGO_EMAIL_FROM`
Для логического бэкапа базы через Django используй команду: ## Быстрый старт
### 1. Подготовить `.env`
Скопируй шаблон и заполни значения:
```bash ```bash
cd cadpoint
python manage.py backup_db
```
По умолчанию файл дампа сохраняется в `database/backups/`. Восстановление делается обычной командой
`python manage.py loaddata <fixture>.json` в пустую базу после `python manage.py migrate`.
## Замена старых Joomla-ссылок в контенте
Для массовой замены старых внутренних ссылок в HTML-контенте используй management command:
```bash
cd cadpoint
python manage.py replace_legacy_links
```
По умолчанию команда работает в режиме `dry-run`: она только показывает, какие поля и записи
будут изменены. Чтобы записать изменения в базу, добавь флаг:
```bash
cd cadpoint
python manage.py replace_legacy_links --apply
```
Сейчас команда чинит только кросс-ссылки на статьи. Ссылки на картинки и прочие медиа пока
оставлены как есть.
Для нового окружения на Poetry:
```bash
poetry install --with dev
cp .env.sample .env cp .env.sample .env
poetry run python cadpoint/manage.py migrate
poetry run python cadpoint/manage.py runserver
``` ```
Для разработки медиа-файлы и статика лежат в `public/media` и `public/static`. Основные переменные:
`django-debug-toolbar` показывается только при `DJANGO_DEBUG=true` и заходе с локального
адреса (`127.0.0.1` / `localhost`); если нужно, свои IP можно добавить в `DJANGO_INTERNAL_IPS`.
## Сборка CodeMirror 6 - Общие:
- `DJANGO_SECRET_KEY` — секрет Django для подписи сессий, CSRF-токенов, password reset и других подписанных данных. Должен быть уникальным и храниться только в `.env`.
- `DJANGO_DEBUG` — включает режим отладки Django. Для локальной разработки обычно `True`, для прода — `False`.
- `DJANGO_ALLOWED_HOSTS` — список доменов и IP, с которых Django принимает запросы. Значения перечисляются через запятую.
- `DJANGO_ADMINS` — список админов для email-уведомлений о критических ошибках. Формат: `Имя:email@domain` (несколько значений через запятую).
- `DJANGO_CSRF_TRUSTED_ORIGINS` — список доверенных origin для CSRF. Нужен для доменов, с которых разрешены POST-запросы.
- `DJANGO_INTERNAL_IPS` — внутренние IP для `debug_toolbar` в dev-режиме. Обычно достаточно `127.0.0.1` и `::1`.
- `DJANGO_SQLITE_NAME` — имя файла SQLite-базы внутри каталога `database/`. Полный путь собирается через `BASE_DIR.parent / 'database'`.
- `ADMIN_URL` — относительный URL админки. По умолчанию `admin/`, можно заменить на любой другой сегмент вроде `a-d-m-i-n/`.
- `DJANGO_EMAIL_HOST` — SMTP-хост почтового сервера.
- `DJANGO_EMAIL_PORT` — SMTP-порт.
- `DJANGO_EMAIL_HOST_USER` — логин для SMTP.
- `DJANGO_EMAIL_HOST_PASSWORD` — пароль или токен для SMTP.
- `DJANGO_EMAIL_FROM` — адрес отправителя писем. Если не задан, берётся из `DJANGO_EMAIL_HOST_USER`.
- Специфичные для продакшена:
- `HOST_PROJECT_PATH` — полный путь к проекту на прод-хосте. Используется при генерации nginx-конфига, чтобы подставить правильный `alias` для media-файлов.
- `REPO_USER` / `REPO_PASS` — логин и токен/пароль для доступа к приватному registry, откуда Watchtower подтягивает новый образ.
Когда появится фронтенд-часть CodeMirror 6 админки, её можно пересобирать скриптом Для ориентира:
`frontend-assembly/build-codemirror6.sh`.
Скрипт создаёт временную рабочую папку, ставит зависимости через `npm ci`, собирает - `DJANGO_DEBUG` управляет самим Django.
минимизированный бандл и затем сам удаляет временные `src/` и `node_modules/`. - `DEBUG` в `docker-compose.local.yml` — это служебная переменная контейнера, но в проекте используется именно `DJANGO_DEBUG`.
В проекте остаётся только готовая статика: - `DJANGO_SETTINGS_MODULE` и `PYTHONUNBUFFERED` задаются в Docker Compose и обычно не трогаются вручную.
* `public/static/codemirror/editor.js` ### 2. Локальная разработка
```bash
docker compose -f docker-compose.local.yml up --build
```
После старта сайт будет доступен на:
```text
http://127.0.0.1:8055
```
### 3. Продакшен на сервере
На сервере должен быть:
- установлен Docker;
- настроен внешний `nginx` на хосте;
- подготовлен `.env`;
- доступен приватный registry с образом проекта.
- в корне проекта на хосте заранее созданы каталоги `database/`, `config/` и `media/` — они монтируются в контейнер как bind-mount'ы.
Запуск: Запуск:
```bash
docker compose -f docker-compose.prod.yml up -d
```
Backend в контейнере слушает только localhost хоста:
```text
127.0.0.1:8050
```
А уже внешний `nginx` проксирует домен на этот порт.
## Где лежат данные
- `public/static/` — исходники статики.
- `public/staticfiles/` — результат `collectstatic`.
- `public/media/` — загруженные файлы и служебные error-pages.
- `database/` — SQLite-файл и бэкапы.
## Основные команды
### Миграции
Локально:
```bash
docker compose -f docker-compose.local.yml exec web python manage.py migrate
```
На проде:
```bash
docker compose -f docker-compose.prod.yml exec web python manage.py migrate
```
### Django shell
```bash
docker compose -f docker-compose.local.yml exec web python manage.py shell
```
### Логи
```bash
docker compose -f docker-compose.prod.yml logs -f web
```
### Бэкап базы
```bash
docker compose -f docker-compose.prod.yml exec web python manage.py backup_db
```
### Восстановление fixture
После `migrate` в пустую базу:
```bash
docker compose -f docker-compose.prod.yml exec web python manage.py loaddata <fixture>.json
```
## Замена старых Joomla-ссылок
Для массовой замены старых внутренних ссылок в HTML-контенте есть management command:
```bash
docker compose -f docker-compose.prod.yml exec web python manage.py replace_legacy_links
```
По умолчанию команда работает в режиме `dry-run`.
Чтобы применить изменения:
```bash
docker compose -f docker-compose.prod.yml exec web python manage.py replace_legacy_links --apply
```
Сейчас команда чинит только кросс-ссылки на статьи. Ссылки на картинки и прочие медиа пока остаются как есть.
## CodeMirror 6 в админке
Редактор админки собирается отдельно из npm-части.
Исходники и скрипт сборки лежат в `frontend-assembly/`.
Сборка:
```bash ```bash
bash ./frontend-assembly/build-codemirror6.sh bash ./frontend-assembly/build-codemirror6.sh
``` ```
Результат сборки — только готовый бандл:
```text
public/static/codemirror/editor.js
```
## Заметки по развертыванию
- Главный источник правды по запуску — `docker-compose.local.yml` и `docker-compose.prod.yml`.
- Секреты не храним в репозитории: используем `.env`.

View File

@@ -57,8 +57,6 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.sitemaps', 'django.contrib.sitemaps',
# Панель отладки показываем только в dev-окружении при `DEBUG=True`.
'debug_toolbar',
'django_select2', 'django_select2',
'easy_thumbnails', 'easy_thumbnails',
'filer.apps.FilerConfig', 'filer.apps.FilerConfig',
@@ -70,8 +68,6 @@ INSTALLED_APPS = [
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
# Middleware нужен, иначе панель debug toolbar просто не влезет в response.
'debug_toolbar.middleware.DebugToolbarMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
@@ -160,6 +156,18 @@ CSRF_TRUSTED_ORIGINS = env.list('DJANGO_CSRF_TRUSTED_ORIGINS', default=[])
# Внутренние адреса для debug toolbar: локальный браузер и loopback. # Внутренние адреса для debug toolbar: локальный браузер и loopback.
INTERNAL_IPS = env.list('DJANGO_INTERNAL_IPS', default=['127.0.0.1', '::1']) INTERNAL_IPS = env.list('DJANGO_INTERNAL_IPS', default=['127.0.0.1', '::1'])
# Django 5 требует явное описание хранилищ.
# `default` нужен для загружаемых файлов (filer, FileField, ImageField) и смотрит в `MEDIA_ROOT`.
# `staticfiles` остаётся отдельно: в dev используется обычная статика Django, в prod — WhiteNoise.
STORAGES = {
'default': {
'BACKEND': 'django.core.files.storage.FileSystemStorage',
'OPTIONS': {
'location': MEDIA_ROOT,
},
},
}
# Параметры Select2 в админке. # Параметры Select2 в админке.
# Держим их здесь, чтобы не размазывать магические числа по `admin.py`. # Держим их здесь, чтобы не размазывать магические числа по `admin.py`.
SELECT2_AJAX_DELAY_MS = 250 SELECT2_AJAX_DELAY_MS = 250
@@ -233,3 +241,29 @@ NUM_NAV_ITEMS_IN_PAGE = 7
# Число статей (заголовок + тизер) на странице # Число статей (заголовок + тизер) на странице
NUM_ITEMS_IN_PAGE = NUM_NAV_ITEMS_IN_PAGE NUM_ITEMS_IN_PAGE = NUM_NAV_ITEMS_IN_PAGE
if DEBUG:
# В деве оставляем стандартную отдачу статики Django без WhiteNoise.
STORAGES['staticfiles'] = {
'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage',
}
# Django Debug Toolbar нужен только в dev
def _show_debug_toolbar(request):
"""Скрывает debug toolbar внутри админки Django"""
return not request.path.startswith(f'/{ADMIN_URL}')
INSTALLED_APPS.append('debug_toolbar')
MIDDLEWARE.insert(1, 'debug_toolbar.middleware.DebugToolbarMiddleware')
DEBUG_TOOLBAR_CONFIG = {
'SHOW_TOOLBAR_CALLBACK': _show_debug_toolbar,
}
else:
# В проде WhiteNoise обслуживает собранную статику и файлы из `public`.
MIDDLEWARE.insert(1, 'whitenoise.middleware.WhiteNoiseMiddleware')
STORAGES['staticfiles'] = {
'BACKEND': 'whitenoise.storage.CompressedManifestStaticFilesStorage',
}
# Конфигурация WhiteNoise для обслуживания статических файлов и файлов из /public (например,
# robots.txt, favicon.ico и т.п.)
WHITENOISE_ROOT = PUBLIC_DIR

View File

@@ -62,7 +62,43 @@ handler403 = 'web.views.handler403'
handler500 = 'web.views.handler500' handler500 = 'web.views.handler500'
if settings.DEBUG: if settings.DEBUG:
import mimetypes
import debug_toolbar import debug_toolbar
from django.views.static import serve
def _serve_public_root_file(request, path):
"""Отдаёт файлы из корня `public` в dev-режиме в utf-8."""
response = serve(request, path, document_root=settings.PUBLIC_DIR)
content_type, _ = mimetypes.guess_type(path)
if content_type:
if content_type.startswith('text/'):
response['Content-Type'] = f'{content_type}; charset=utf-8'
else:
response['Content-Type'] = content_type
elif path.endswith('.txt'):
response['Content-Type'] = 'text/plain; charset=utf-8'
elif path.endswith('.html'):
response['Content-Type'] = 'text/html; charset=utf-8'
return response
def _iter_public_root_files():
"""Находит все обычные файлы в корне `public`, кроме служебных артефактов."""
for file_path in sorted(settings.PUBLIC_DIR.iterdir()):
if not file_path.is_file():
continue
if file_path.name.startswith('.'):
continue
if file_path.name == 'README.md':
continue
yield file_path.name
PUBLIC_ROOT_URLPATTERNS = [
path(filename, _serve_public_root_file, {'path': filename})
for filename in _iter_public_root_files()
]
urlpatterns = [path('__debug__/', include(debug_toolbar.urls)), ] + urlpatterns 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 += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@@ -14,15 +14,15 @@
<meta name="generator" content="handcraft" /> <meta name="generator" content="handcraft" />
<title>CADpoint.ru - http 500 error</title> <title>CADpoint.ru - http 500 error</title>
<meta name="theme-color" content="#F5F5F5" /> <meta name="theme-color" content="#F5F5F5" />
<link rel="icon" type="image/svg+xml" href="/static/svgs/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/media/_error/svgs/favicon.svg" />
<link rel="icon" type="image/png" href="/static/img/favicon.png" /> <link rel="icon" type="image/png" href="/media/_error/img/favicon.png" />
<link rel="shortcut icon" type="image/x-icon" href="/static/img/favicon.ico" /> <link rel="shortcut icon" type="image/x-icon" href="/media/_error/img/favicon.ico" />
</head> </head>
<body> <body>
<div style="width:100%;height:80%;display:table;position:absolute;top:0;left:0;"> <div style="width:100%;height:80%;display:table;position:absolute;top:0;left:0;">
<div style="display:table-cell;text-align:center;vertical-align:middle;"> <div style="display:table-cell;text-align:center;vertical-align:middle;">
<div style="display:inline-block;"> <div style="display:inline-block;">
<img src="/static/svgs/cappoint_under_reconstruction.svg" <img src="/media/_error/svgs/cappoint_under_reconstruction.svg"
alt="cadpoint.ru - under reconstruction" style="width: 80vw;"/> alt="cadpoint.ru - under reconstruction" style="width: 80vw;"/>
<pre>сайт cadpoint.ru реконструируется... подождите, скоро все вся станет не так как прежде...</pre> <pre>сайт cadpoint.ru реконструируется... подождите, скоро все вся станет не так как прежде...</pre>
</div> </div>

View File

@@ -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; # Всегда редиректим на основной домен
# }

View File

@@ -1,189 +0,0 @@
## Установка (компиляция) версии Python 3.8.6
Проект создан на версии Python 3.8.6. Скомпилируем необходимую версию Python.
1. ВХОДИМ ЧЕРЗ SSH ЧЕРЕЗ LOGIN/PWD СВОЕГО АККАУНТА.
2. Создадим папку `tmp` (скорее всего уже создана)
3. Перейдем в эту папку
4. Скачаем tgz-архив с исходными файлами Python
5. Распакуем архив с помощью `tar`
6. Перейдем в папку `Python-3.8.6`, созданную при разархивации.
7. Сконфигурируем будущую компиляцию на размещение готовой версии Python в папку `~/opt/python-3.8.6`
8. Компилируем Python (в том числе будут запущены тесты)
9. Устанавливаем Python 3.8.6
Контакт для уведомлений: <notification_email>
```
cd ~
mkdir tmp
cd tmp
wget https://www.python.org/ftp/python/3.8.6/Python-3.8.6.tgz
tar zxvf Python-3.8.6.tgz
cd Python-3.8.6
./configure --prefix=$HOME/opt/python-3.8.6 --enable-optimizations
make
make install
```
В результате установлена нужная нам версия python установлена в папку `~/<site_root>` (`/home/<hosting_user>/opt/python-3.8.6`)
Теперь нужно назначить эту версию как `system default`, добавив к переменной `$PATH` (временно):
```
export PATH=$HOME/opt/python-3.8.6/bin:$PATH
```
------------------------------
_Также можно добавить эту строку в файл `.bashrc` и/или `.bash_profile` в домашней директории `/home/<hosting_user>`. Это нужно, чтобы сделать так, чтобы этот python всегда заменял версию которая есть на сервере._
-------------------------------
Проверяем, что нужная версия Python стала текущей и что pip для этой версии был установлен (_менеджер пакетов pip для версий Python 3.x входит в поставку... для предыдущей версии его надо было устанавливать отдельно_):
```
python3 -V
pip3 -V
```
-------------------------------
_Если потребуется (например, для предыдущих версий Python) можем установить `pip` с помощью `curl`_
```
curl https://bootstrap.pypa.io/get-pip.py > ~/tmp/get-pip.py
python ~/tmp/get-pip.py
```
-------------------------------
## Настройка виртуального окружения проекта
Чтобы "заморозить" установленную версию Python в виртуальном окружении `virtualenv`:
```
pip3 install virtualenv
```
Через панель управления хостингом __Domains -> Manage Domains -> Add Hosting to a Domain/Sub-Domain__ создадим поддомен __cadpoint.ru__ (без создания нового пользователя). В нашем домашнем каталоге будет создана папка `~/<site_root>`. В этой папке будет лежать `passenger_wsgi.py`, также есть папка `public` в которой будут лежать статичные файлы не требующие обработки CGI (media, static и пр.)
Теперь создадим виртуальное окружение в папке нашего сайта (`$HOME/<site_root>`):
```
virtualenv -p python3 $HOME/<site_root>/env
```
Активируем созданное виртуальное окружение:
```
source $HOME/<site_root>/env/bin/activate
```
Проверить, что теперь мы работаем в виртуальном окружении можно дав команды:
```
python -V
pip -V
```
Мы увидим, что срабатывают нужные нам версии (т.е. не надо использовать `python3` и `pip3`).
## Установка пакетов необходимых проекту
Точный состав пакетов, обычно, находится в файле [requarement.txt](dicquo/requarement.txt). Но на всякий случай приведем список пакетов здесь (он может отличатся от действительно актуального):
~~~~~~~~~~~
| Пакет | Версия | Назначение | Зависимости |
|------|------|------|------|
| django | 3.1.3 | Фреймворк Django | притащит с собой пакеты: __asgiref-3.3.0__, __pytz-2020.4__, __sqlparse-0.4.1__
| django-taggit | 1.3.0 | Система тегов для Django | нет
| pillow | 8.0.1 | Пакет работы с графическими файлами
| pytils | 0.4.4 | Пакет рускоязычной транслитерации, работы с числительными, склонениями числительных и временными диаппазонами (для Python 3.x) | нет
| typus | 0.2.2 | типограф | нет
| urllib3 | 1.25.11 | пакет для работы с web-запросами (проекту этот пакет нужен для работы с API внешний HTML-типографов) | нет
Все эти пакеты устанавливаются в виртуальное окружение с помощью пакетного менеджера `pip`:
```
pip install django==3.1.3
pip install django-taggit==1.3.0
pip install pillow==8.0.1
pip install pytils==0.4.4
pip install typus==0.2.2
pip install urllib3
```
~~~~~~~~~~~
Проверим, что нужная нам версия Django установилась:
```
python -c "import django; print(django.get_version())"
```
## Копируем проект на хостинг
На момент написания данной документации структура файлов и каталогов проекта в папке `cadpoint.ru` выглядела примерно так:
```
.
|-- passenger_wsgi.py
|-- cadpoint
| |-- db.sqlite3
| |-- manage.py
| |-- cadpoint
| | |-- __init__.py
| | |-- asgi.py
| | |-- my_secret.py # хранится только локально, в Git не коммитится
| | |-- settings.py
| | |-- urls.py
| | `-- wsgi.py
| |-- templates
| | |-- base.html
| | |-- blocks
| | | `-- tecnical_info.html
| | `-- index.html
| `-- web
|-- public
`-- tmp
`-- restart.txt
```
Далее нам надо скопировать статические файлы админки Django в папку статических файлов хостинга:
```
cd ~/<site_root>/dicquo
python manage.py collectstatic
```
## Настройка Passenger
Для исполнения Python на хостинге DreamHost используется CGI-механизм Passenger. Чтобы его настроить для нашего проекта в папке сайта `~/cadpoint.ru` нужно разметить файл `passenger_wsgi.py` следующего содержания ([см. документацию DreamHost](https://help.dreamhost.com/hc/en-us/articles/360002341572-Creating-a-Django-project)):
```python
#!/home/<hosting_user>/<site_root>/env/bin/python3
import sys, os
INTERP = "/home/<hosting_user>/<site_root>/env/bin/python3"
#INTERP is present twice so that the new python interpreter
#knows the actual executable path
if sys.executable != INTERP:
os.execl(INTERP, INTERP, *sys.argv)
cwd = os.getcwd()
sys.path.append(cwd)
sys.path.append(cwd + '/cadpoint') #You must add your project here
sys.path.insert(0,cwd+'/env/bin')
# sys.path.insert(0,cwd+'/env/lib/python3.8/site-packages/django')
sys.path.insert(0,cwd+'/env/lib/python3.8/site-packages')
os.environ['DJANGO_SETTINGS_MODULE'] = "cadpoint.settings"
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
```
После этого наш сайт должен зарабоать.
Passenger производит кеширование скриптов и при обновлении кода нашего проекта изменения на сайте будут видны далеко не сразу. Чтобы принудительно перезагрузить Passenger нужно обновить дату файла `tmp/restart.txt` в папке нашего проекта ([см. документацию DreamHost](https://help.dreamhost.com/hc/en-us/articles/216385637-How-do-I-enable-Passenger-on-my-domain-)).
Сначала создадим соответствующий каталог:
```
cd ~/cadpoint.ru
mkdir -p tmp
```
Обновлять `restart.txt` можно командой:
```
touch ~/cadpoint.ru/tmp/restart.txt
```
## Дополнительно
Стоит включить ssl-сертификат для сайта. В панели управления DreamHost __Domains --> SSL/TLS Certificates__

File diff suppressed because it is too large Load Diff

65
docker-compose.local.yml Normal file
View File

@@ -0,0 +1,65 @@
# ==============================================================================
# Docker Compose для РАЗРАБОТКИ (Local Development)
# Этот файл содержит настройки для локальной работы (live reload, debug).
# Запуск: docker compose -f docker-compose.local up --build
# ==============================================================================
services:
web:
# Имя контейнера для удобства
container_name: cadpoint-backend-dev
# Сборка из текущей директории
build: .
# Проброс портов (чтобы сайт был доступен на localhost:8055)
ports:
- "8055:8000"
# 1. КОМАНДА ЗАПУСКА (Dev режим)
# Используем --reload для авто-перезагрузки при изменении кода.
# Уменьшаем число воркеров до 1 (ресурсы dev-машины можно не экономит, но одного достаточно).
# Убираем collectstatic (в dev Django сам может отдавать статику или она нам не так важна сжатой)
# Но миграции оставляем, чтобы база была актуальной.
command: >
sh -c "python manage.py migrate --noinput &&
python -m gunicorn --workers 1 --bind 0.0.0.0:8000 --reload cadpoint.wsgi:application"
# 2. МОНТИРОВАНИЕ КОДА (Live Reload)
# Подключаем локальные папки внутрь контейнера, чтобы Gunicorn видел изменения без пересборки образа.
volumes:
# Монтируем основной код проекта.
# Так как web, templates и manage.py лежат внутри cadpoint/, одного этого маунта достаточно.
- ./cadpoint:/home/app/web/cadpoint
# Монтируем всю папку 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/cadpoint, значит путь к базе: /home/app/web/database/db.sqlite3
- ./database:/home/app/web/database
# 3. ПЕРЕМЕННЫЕ ОКРУЖЕНИЯ
env_file:
# файл с переменными окружения для разработки
- .env
environment:
# на всякий случай, принудительно включаем DEBUG и DEBUG-уровень логов (вдруг в .env что-то не так)
- DEBUG=True
- DJANGO_LOG_LEVEL=DEBUG
# В dev нам не нужно ограничивать буферизацию так строго, но не помешает.
# 4. РЕСУРСЫ (Без лимитов для разработки)
# Удаляем секцию ограничений, чтобы локально использовать все доступные ресурсы хоста.
# deploy:
# resources:
# limits:
# cpus: ...
# memory: ...
# mem_limit: ...

151
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,151 @@
# ==============================================================================
# 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\" /home/app/web/config/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" &&
python -m 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. Пользователь и права
# На первом старте контейнеру нужны права на инициализацию 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=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"

50
poetry.lock generated
View File

@@ -392,6 +392,29 @@ beautifulsoup4 = ">=4.10.0"
lxml = ">=4.9.0" lxml = ">=4.9.0"
regex = ">=2022.1.18" regex = ">=2022.1.18"
[[package]]
name = "gunicorn"
version = "25.3.0"
description = "WSGI HTTP Server for UNIX"
optional = false
python-versions = ">=3.10"
files = [
{file = "gunicorn-25.3.0-py3-none-any.whl", hash = "sha256:cacea387dab08cd6776501621c295a904fe8e3b7aae9a1a3cbb26f4e7ed54660"},
{file = "gunicorn-25.3.0.tar.gz", hash = "sha256:f74e1b2f9f76f6cd1ca01198968bd2dd65830edc24b6e8e4d78de8320e2fe889"},
]
[package.dependencies]
packaging = "*"
[package.extras]
eventlet = ["eventlet (>=0.40.3)"]
fast = ["gunicorn_h1c (>=0.6.3)"]
gevent = ["gevent (>=24.10.1)"]
http2 = ["h2 (>=4.1.0)"]
setproctitle = ["setproctitle"]
testing = ["coverage", "eventlet (>=0.40.3)", "gevent (>=24.10.1)", "h2 (>=4.1.0)", "httpx[http2]", "pytest", "pytest-asyncio", "pytest-cov", "uvloop (>=0.19.0)"]
tornado = ["tornado (>=6.5.0)"]
[[package]] [[package]]
name = "lxml" name = "lxml"
version = "6.0.2" version = "6.0.2"
@@ -547,6 +570,17 @@ html-clean = ["lxml_html_clean"]
html5 = ["html5lib"] html5 = ["html5lib"]
htmlsoup = ["BeautifulSoup4"] htmlsoup = ["BeautifulSoup4"]
[[package]]
name = "packaging"
version = "26.0"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"},
{file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"},
]
[[package]] [[package]]
name = "pillow" name = "pillow"
version = "12.2.0" version = "12.2.0"
@@ -941,7 +975,21 @@ files = [
{file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
] ]
[[package]]
name = "whitenoise"
version = "6.12.0"
description = "Radically simplified static file serving for WSGI applications"
optional = false
python-versions = ">=3.10"
files = [
{file = "whitenoise-6.12.0-py3-none-any.whl", hash = "sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2"},
{file = "whitenoise-6.12.0.tar.gz", hash = "sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad"},
]
[package.extras]
brotli = ["brotli"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.12,<3.13" python-versions = ">=3.12,<3.13"
content-hash = "8284fc2ef5f2a06d27b41da40cc2067920b8fd5fed8f23621b777a15d8ca4559" content-hash = "f0a03e4a068c519c6f13ec9816ea519c95ff650b1aa85915f17bc8ca364bf20a"

View File

@@ -3,7 +3,7 @@
Эта папка предназначена для хранения статических файлов, таких как изображения, стили CSS и JavaScript файлы. Эта папка предназначена для хранения статических файлов, таких как изображения, стили CSS и JavaScript файлы.
Папка будет внутри контейнера, а файлы внутри папки будут доступны с помощью gunicorn и whitenoise на хосте. Папка будет внутри контейнера, а файлы внутри папки будут доступны с помощью gunicorn и whitenoise на хосте.
Но в случае сбоя контейнера, ошибок Djанго и ошибок 404 при доступе в media (которые будут в папке `media` на Но в случае сбоя контейнера, ошибок Django и ошибок 404 при доступе в media (которые будут в папке `media` на
внешнем хосте с доступом через nginx), то эти файлы могут стать недоступны. Таким образом, файлы необхдимые внешнем хосте с доступом через nginx), то эти файлы могут стать недоступны. Таким образом, файлы необхдимые
для отображения кастовых страниц ошибок 400, 403, 404, 500 и других должны быть скопированы на внешний хост, для отображения кастовых страниц ошибок 400, 403, 404, 500 и других должны быть скопированы на внешний хост,
в папку `media/_error` в момент запуска контейнера (с помощью `entrypoint.sh` или инструкций `command` в папку `media/_error` в момент запуска контейнера (с помощью `entrypoint.sh` или инструкций `command`

View File

@@ -24,6 +24,8 @@ django-mptt = "^0.18.0"
pytils = "^0.4.4" pytils = "^0.4.4"
django-select2 = "^8.4.8" django-select2 = "^8.4.8"
etpgrf = "^0.1.6" etpgrf = "^0.1.6"
gunicorn = "^25.3.0"
whitenoise = "^6.12.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
django-debug-toolbar = "^6.3" django-debug-toolbar = "^6.3"