From 4669f53b69b0503e73de87374dc4c3d92f31186b Mon Sep 17 00:00:00 2001 From: erjemin Date: Sun, 12 Apr 2026 22:01:38 +0300 Subject: [PATCH 01/16] =?UTF-8?q?mod:=20=D1=83=D1=82=D0=BE=D1=87=D0=BD?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0?= =?UTF-8?q?=20.dockerignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..946b975 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,46 @@ +# Исключаем мусор и локальные артефакты, чтобы Docker-контекст был компактным. + +# Git и IDE-файлы в образ не нужны. +.git +.github +.idea +.DS_Store + +# Секреты и локальные настройки не должны попадать в контейнерный контекст. +.env +.env.* + +# Виртуальное окружение и служебные артефакты Python. +.venv/ +__pycache__/ +*.py[cod] +*.log +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage* +htmlcov/ +.tox/ + +# Локальные базы и дампы SQLite в контейнер не тащим. +*.sqlite3 +database/ + +# Локальная сборка фронтенда пока не нужна в Docker-контексте. +# Если позже соберём frontend внутри Docker, это правило можно пересмотреть. +frontend-assembly/ + +# Загруженные медиа-файлы монтируются отдельно и не должны раздувать контекст. +public/media/ + +# Документация и служебные git-ignore-файлы не нужны в runtime-образе. +*.md +**/.gitignore + +# Будущие Dockerfile и основной compose-файл обычно храним в репозитории, +# поэтому их НЕ игнорируем. Игнорируем только локальные override-варианты. +docker-compose.override.yml +compose.override.yml +docker-compose.local.yml +compose.local.yml + From 57d3f1ea2c81a55f3ed2827bf545877cd368ea9e Mon Sep 17 00:00:00 2001 From: erjemin Date: Mon, 13 Apr 2026 17:41:35 +0300 Subject: [PATCH 02/16] minor: - --- public/static/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/static/README.md b/public/static/README.md index 8fc3bb6..18b0203 100644 --- a/public/static/README.md +++ b/public/static/README.md @@ -3,7 +3,7 @@ Эта папка предназначена для хранения статических файлов, таких как изображения, стили CSS и JavaScript файлы. Папка будет внутри контейнера, а файлы внутри папки будут доступны с помощью gunicorn и whitenoise на хосте. -Но в случае сбоя контейнера, ошибок Djанго и ошибок 404 при доступе в media (которые будут в папке `media` на +Но в случае сбоя контейнера, ошибок Django и ошибок 404 при доступе в media (которые будут в папке `media` на внешнем хосте с доступом через nginx), то эти файлы могут стать недоступны. Таким образом, файлы необхдимые для отображения кастовых страниц ошибок 400, 403, 404, 500 и других должны быть скопированы на внешний хост, в папку `media/_error` в момент запуска контейнера (с помощью `entrypoint.sh` или инструкций `command` From 75302a563a6032c8bad9a2eb9e505a01c1086961 Mon Sep 17 00:00:00 2001 From: erjemin Date: Mon, 13 Apr 2026 18:59:16 +0300 Subject: [PATCH 03/16] mod: adjust debug and static settings --- cadpoint/cadpoint/settings.py | 34 +++++++++++++++++++++--- poetry.lock | 50 ++++++++++++++++++++++++++++++++++- pyproject.toml | 2 ++ 3 files changed, 81 insertions(+), 5 deletions(-) diff --git a/cadpoint/cadpoint/settings.py b/cadpoint/cadpoint/settings.py index 624e862..c93fbd8 100644 --- a/cadpoint/cadpoint/settings.py +++ b/cadpoint/cadpoint/settings.py @@ -57,8 +57,6 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.sitemaps', - # Панель отладки показываем только в dev-окружении при `DEBUG=True`. - 'debug_toolbar', 'django_select2', 'easy_thumbnails', 'filer.apps.FilerConfig', @@ -70,8 +68,6 @@ INSTALLED_APPS = [ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', - # Middleware нужен, иначе панель debug toolbar просто не влезет в response. - 'debug_toolbar.middleware.DebugToolbarMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -233,3 +229,33 @@ NUM_NAV_ITEMS_IN_PAGE = 7 # Число статей (заголовок + тизер) на странице 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 diff --git a/poetry.lock b/poetry.lock index 208d075..492f955 100644 --- a/poetry.lock +++ b/poetry.lock @@ -392,6 +392,29 @@ beautifulsoup4 = ">=4.10.0" lxml = ">=4.9.0" 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]] name = "lxml" version = "6.0.2" @@ -547,6 +570,17 @@ html-clean = ["lxml_html_clean"] html5 = ["html5lib"] 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]] name = "pillow" version = "12.2.0" @@ -941,7 +975,21 @@ files = [ {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] lock-version = "2.0" python-versions = ">=3.12,<3.13" -content-hash = "8284fc2ef5f2a06d27b41da40cc2067920b8fd5fed8f23621b777a15d8ca4559" +content-hash = "f0a03e4a068c519c6f13ec9816ea519c95ff650b1aa85915f17bc8ca364bf20a" diff --git a/pyproject.toml b/pyproject.toml index a6d5c85..521ee80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,8 @@ django-mptt = "^0.18.0" pytils = "^0.4.4" django-select2 = "^8.4.8" etpgrf = "^0.1.6" +gunicorn = "^25.3.0" +whitenoise = "^6.12.0" [tool.poetry.group.dev.dependencies] django-debug-toolbar = "^6.3" From 19bf4fb293d369f943a8d9b7379169cfa111f47d Mon Sep 17 00:00:00 2001 From: erjemin Date: Tue, 14 Apr 2026 13:33:10 +0300 Subject: [PATCH 04/16] fix: restore dev root file helpers --- cadpoint/cadpoint/urls.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/cadpoint/cadpoint/urls.py b/cadpoint/cadpoint/urls.py index ac88c02..f26f5ac 100644 --- a/cadpoint/cadpoint/urls.py +++ b/cadpoint/cadpoint/urls.py @@ -62,7 +62,43 @@ handler403 = 'web.views.handler403' handler500 = 'web.views.handler500' if settings.DEBUG: + import mimetypes 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 = [*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.STATIC_URL, document_root=settings.STATIC_ROOT) From 11294a6c8c58e603711786b6562ab6282920ec94 Mon Sep 17 00:00:00 2001 From: erjemin Date: Tue, 14 Apr 2026 13:37:53 +0300 Subject: [PATCH 05/16] =?UTF-8?q?mod:=20=D0=A8=D0=B0=D0=B1=D0=BB=D0=BE?= =?UTF-8?q?=D0=BD=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B5=D0=B4=D0=B5=D1=82=20?= =?UTF-8?q?=D0=B8=D0=B7=20=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=B9=D0=BD=D0=B5?= =?UTF-8?q?=D1=80=D0=B0=20=D0=BD=D0=B0=20=D1=85=D0=BE=D1=81=D1=82=20=D0=B8?= =?UTF-8?q?=20=D0=B1=D1=83=D0=B4=D0=B5=D1=82=20=D0=BE=D1=82=D0=B4=D0=B0?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D1=8C=D1=81=D1=8F=20nginx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cadpoint/templates/under_reconstruction.jinja2 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cadpoint/templates/under_reconstruction.jinja2 b/cadpoint/templates/under_reconstruction.jinja2 index 94ff654..dcac0ca 100644 --- a/cadpoint/templates/under_reconstruction.jinja2 +++ b/cadpoint/templates/under_reconstruction.jinja2 @@ -14,15 +14,15 @@ CADpoint.ru - http 500 error - - - + + +
- cadpoint.ru - under reconstruction
сайт cadpoint.ru реконструируется... подождите, скоро все вся станет не так как прежде...
From 2eb6636ad15324c04e9de286ece0b15e2db41b7b Mon Sep 17 00:00:00 2001 From: erjemin Date: Tue, 14 Apr 2026 13:46:32 +0300 Subject: [PATCH 06/16] mod: add docker dev setup --- Dockerfile | 90 ++++++++++++++++++++++++++++++++++++++++ docker-compose.local.yml | 65 +++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 Dockerfile create mode 100644 docker-compose.local.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..283767c --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 0000000..d4ce512 --- /dev/null +++ b/docker-compose.local.yml @@ -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 && + 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: ... + From 5ac33c2a5db0304657a49582405b050b64f69b16 Mon Sep 17 00:00:00 2001 From: erjemin Date: Tue, 14 Apr 2026 14:15:06 +0300 Subject: [PATCH 07/16] =?UTF-8?q?add:=20HOST=5FPROJECT=5FPATH=20=D0=B8=20R?= =?UTF-8?q?EPO=5FUSER/REPO=5FPASS=20=D0=B4=D0=BB=D1=8F=20ci/cd?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.sample | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.env.sample b/.env.sample index b25ec0c..452baef 100644 --- a/.env.sample +++ b/.env.sample @@ -20,3 +20,16 @@ DJANGO_EMAIL_FROM=you@email.com # URL для доступа к админке Django (можно сменить для безопасности, чтобы боты не могли её найти) 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] + From cb28acfaf24beaadd447e94e1fc636dc1c60886c Mon Sep 17 00:00:00 2001 From: erjemin Date: Tue, 14 Apr 2026 15:41:09 +0300 Subject: [PATCH 08/16] =?UTF-8?q?mod:=20=D0=A8=D0=B0=D0=B1=D0=BB=D0=BE?= =?UTF-8?q?=D0=BD=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B5=D0=B4=D0=B5=D1=82=20?= =?UTF-8?q?=D0=B8=D0=B7=20=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=B9=D0=BD=D0=B5?= =?UTF-8?q?=D1=80=D0=B0=20=D0=BD=D0=B0=20=D1=85=D0=BE=D1=81=D1=82=20=D0=B8?= =?UTF-8?q?=20=D0=B1=D1=83=D0=B4=D0=B5=D1=82=20=D0=BE=D1=82=D0=B4=D0=B0?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D1=8C=D1=81=D1=8F=20nginx=20(=D0=BA=D0=B0?= =?UTF-8?q?=D0=BA=20.html)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{under_reconstruction.jinja2 => under_reconstruction.html} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cadpoint/templates/{under_reconstruction.jinja2 => under_reconstruction.html} (100%) diff --git a/cadpoint/templates/under_reconstruction.jinja2 b/cadpoint/templates/under_reconstruction.html similarity index 100% rename from cadpoint/templates/under_reconstruction.jinja2 rename to cadpoint/templates/under_reconstruction.html From 5d7470ac7d76c3c4f91da1f2c05677a323c4cd9f Mon Sep 17 00:00:00 2001 From: erjemin Date: Tue, 14 Apr 2026 16:28:12 +0300 Subject: [PATCH 09/16] mod: update production docker deployment --- .dockerignore | 12 +- .gitea/workflows/docker-publish.yaml | 72 +++++++++ .../nginx/cadpoint-app--external-nginx.conf | 147 +++++++++++++++++ docker-compose.prod.yml | 148 ++++++++++++++++++ 4 files changed, 373 insertions(+), 6 deletions(-) create mode 100644 .gitea/workflows/docker-publish.yaml create mode 100644 config/nginx/cadpoint-app--external-nginx.conf create mode 100644 docker-compose.prod.yml diff --git a/.dockerignore b/.dockerignore index 946b975..49d83c1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,6 +9,7 @@ # Секреты и локальные настройки не должны попадать в контейнерный контекст. .env .env.* +.env.sample # Виртуальное окружение и служебные артефакты Python. .venv/ @@ -25,6 +26,7 @@ htmlcov/ # Локальные базы и дампы SQLite в контейнер не тащим. *.sqlite3 database/ +media/ # Локальная сборка фронтенда пока не нужна в Docker-контексте. # Если позже соберём frontend внутри Docker, это правило можно пересмотреть. @@ -37,10 +39,8 @@ public/media/ *.md **/.gitignore -# Будущие Dockerfile и основной compose-файл обычно храним в репозитории, -# поэтому их НЕ игнорируем. Игнорируем только локальные override-варианты. -docker-compose.override.yml -compose.override.yml -docker-compose.local.yml -compose.local.yml +# Репозиторные и оркестрационные файлы не нужны внутри runtime-образа. +.gitea/ +Dockerfile +docker-compose*.yml diff --git a/.gitea/workflows/docker-publish.yaml b/.gitea/workflows/docker-publish.yaml new file mode 100644 index 0000000..0422340 --- /dev/null +++ b/.gitea/workflows/docker-publish.yaml @@ -0,0 +1,72 @@ +name: Build and Push Docker Image +run-name: Build and Push Docker Image ${{ github.ref_name }} + +on: + push: + # Запускать сборку только при создании тега, начинающегося с 'v' (например, v1.0.0, v2.3.1) + tags: + - 'v*' + +env: + REGISTRY: git.cube2.ru + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest # Или метка вашего раннера, если он специфичный (например, macos или self-hosted) + container: + image: catthehacker/ubuntu:act-latest + + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Настройка QEMU для мультиплатформенной сборки (если нужно собирать под разные архитектуры) + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + # Настройка Docker Buildx (обязательно для build-push-action) + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + # Логин в реестр Gitea + - name: Log in to the Container registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.REPO_USER }} + password: ${{ secrets.REPO_PASS }} + + # Извлечение метаданных (тегов и лейблов) для Docker + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=tag + type=raw,value=latest,enable=${{ github.ref_type == 'tag' }} + + # Сборка и отправка образа + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: . + file: Dockerfile + push: true + # Собираем под текущую архитектуру (linux/amd64). + # Если сервер и MacMini на разных архитектурах (x86 vs ARM), добавьте нужные, например: linux/amd64,linux/arm64 + # platforms: linux/amd64,linux/arm64 + # --- + # Собираем только под linux/amd64 (для скорости) + platforms: linux/amd64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + # ДОБАВЛЕНО для медленного интернета и оптимизации сборки: + cache-from: type=gha + cache-to: type=gha,mode=max + timeout: 900 # 15 минут на всю сборку diff --git a/config/nginx/cadpoint-app--external-nginx.conf b/config/nginx/cadpoint-app--external-nginx.conf new file mode 100644 index 0000000..e8b2eee --- /dev/null +++ b/config/nginx/cadpoint-app--external-nginx.conf @@ -0,0 +1,147 @@ +# config/nginx/cadpoint-app--external-nginx.conf +# ============================================================================== +# ЭТАЛОННЫЙ КОНФИГУРАЦИОННЫЙ ФАЙЛ NGINX (Reverse Proxy для Docker) +# ============================================================================== +# +# ВНИМАНИЕ: +# Этот файл является шаблоном. При первом деплое он копируется в `/home/user/app/cadpoint-site/config/nginx/cadpoint-app--external-nginx.conf`, +# а затем (уже руками) через силинк в `/etc/nginx/sites-available/` и активируется. +# При последующих деплоях он НЕ ПЕРЕЗАПИСЫВАЕТСЯ автоматически, чтобы не затереть SSL-сертификаты и ручные правки. +# +# Если вы изменили этот файл в репозитории и хотите применить изменения на проде: +# вам нужно обновить файл в `/home/user/app/cadpoint-site/config/nginx/cadpoint-app--external-nginx.conf` вручную (diff + copy). +# +# Так же (рядом) будет создан образец этого файла `nginx_cadpoint.conf.example`, который будет обновляться при деплоях +# из репозитория, чтобы вы могли видеть, что изменилось и при необходимости перенести эти изменения на прод. +# +# Предполагаемая структура на сервере: +# /home/user/app/cadpoint-site/ +# ├── docker-compose.yml +# ├── .env +# ├── media/ <-- Сюда Nginx смотрит напрямую (Docker volume) +# └── ... + +# 1. Описываем, где живет наш Django в Docker +upstream cadpoint-django { + # Мы пробрасываем порт 8050 из контейнера наружу (в docker-compose.yml имя сервиса 'web', контейнер 'cadpoint-backend') + server 127.0.0.1:8050; + keepalive_requests 200; +} + +# 2. Конфигурируем сервер +server { + server_name test.cadpoint.ru; # Основное доменное имя + + # Слушаем 80 порт (Certbot потом добавит сюда редирект на 443 и настройки SSL) + listen 80; + listen [::]:80; + + charset utf-8; + client_max_body_size 10M; # Разрешаем загрузку не слишком больших картинок + + # Логи (пути могут отличаться в зависимости от настроек сервера, здесь стандартные для Ubuntu) + access_log /var/log/nginx/cadpoint.access.log; + error_log /var/log/nginx/cadpoint.error.log; + + # --- GZIP (Сжатие) --- + # Очень важно для динамического HTML от Django, который Gunicorn отдает несжатым. + gzip on; + gzip_vary on; # Добавляет заголовок Vary: Accept-Encoding + gzip_proxied any; # Сжимать ответы, даже если мы за прокси + gzip_comp_level 6; # Оптимальный баланс скорость/сжатие + gzip_min_length 1000; # Не сжимать совсем мелочь + # Типы файлов для сжатия (HTML сжимается автоматически, его писать не нужно) + gzip_types + text/plain + text/css + text/xml + text/javascript + application/javascript + application/json + application/xml + application/xml+rss + image/svg+xml + image/x-icon + application/vnd.ms-fontobject + font/woff + font/woff2; + + # --- МЕДИА ФАЙЛЫ (Загруженный контент) --- + # Nginx отдает их напрямую с диска хоста, не дергая Docker. + # Путь должен совпадать с тем, где лежит volume на хост-машине. + # ВАЖНО: Убедитесь, что пользователь nginx (www-data) имеет права на чтение этой папки! + # ТРЕБУЕТСЯ ЗАМЕНА ПРИ ДЕПЛОЕ: /home/user/app/cadpoint-site -> ваш реальный путь + location /media/ { + alias /home/user/app/cadpoint-site/media/; + expires 30d; # Кешируем картинки на месяц + add_header Cache-Control "public, no-transform"; + } + + # --- СТРАНИЦЫ ОШИБОК (Custom Error Pages) --- + # Если Django упал (502) или сработал тайм-аут (504), Nginx должен отдать статический HTML. + # Эти файлы должны лежать в папке, доступной Nginx (например, в `media/_error`). + # + # ВАЖНО: + # 1. Файлы 50x.html (500, 502, 503, 504) копируются в `media/_error` при старте контейнера (см. docker-compose.prod.yml -> command). + # 2. error_page директива перехватывает ошибки от апстрима (Gunicorn). + error_page 500 /500.html; + error_page 502 /502.html; + error_page 503 /503.html; + error_page 504 /504.html; + + location = /500.html { root /home/user/app/cadpoint-site/media/_error; internal; } + location = /502.html { root /home/user/app/cadpoint-site/media/_error; internal; } + location = /503.html { root /home/user/app/cadpoint-site/media/_error; internal; } + location = /504.html { root /home/user/app/cadpoint-site/media/_error; internal; } + + # 404 (и другие) тоже нужно кастомизировать... обычно Django сам отдает 404. + # Но, например, Nginx отдаст 404 при ошике доступа к media-файлам (они храняться на хосте, а не в контейнере) + error_page 400 /400.html; + error_page 401 /401.html; + error_page 403 /403.html; + error_page 404 /404.html; + error_page 413 /413.html; + error_page 429 /429.html; + + location = /400.html { root /home/user/app/cadpoint-site/media/_error; internal; } + location = /401.html { root /home/user/app/cadpoint-site/media/_error; internal; } + location = /403.html { root /home/user/app/cadpoint-site/media/_error; internal; } + location = /404.html { root /home/user/app/cadpoint-site/media/_error; internal; } + location = /413.html { root /home/user/app/cadpoint-site/media/_error; internal; } + location = /429.html { root /home/user/app/cadpoint-site/media/_error; internal; } + + # Указываем единую страницу (на реконструкции) для всех прочих ошибок + error_page 405 406 407 408 409 410 411 412 414 415 416 417 418 421 422 423 424 425 426 428 431 451 /under_reconstruction.html; + location = /under_reconstruction.html { root /home/user/app/cadpoint-site/media/_error; internal; } + + # --- ВСЁ ОСТАЛЬНОЕ (Django + WhiteNoise) --- + # Статика (/static/), robots.txt, favicon.ico и сам сайт обрабатываются внутри контейнера. + # Nginx просто прокидывает запрос внутрь. + location / { + proxy_pass http://cadpoint-django; + + # Передаем правильные заголовки, чтобы Django знал реальный IP и протокол + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # Если нужно чтобы Django обрабатывал и HTTP, и HTTPS, то можно раскомментировать эту строку + # и передавать реальный протокол от клиента + # proxy_set_header X-Forwarded-Proto $scheme; + + # Явно указываем https, потому что клиент всегда приходит по HTTPS к Nginx + # Даже если внутри контейнера это HTTP на 127.0.0.1:8050, для Django это должно быть HTTPS + proxy_set_header X-Forwarded-Proto https; + + # Тайм-ауты (важно для долгих операций, если они есть) + proxy_read_timeout 180s; + proxy_connect_timeout 180s; + } +} + +# 3. Редирект с www на без-www (SEO best practice) +# server { +# server_name www.cadpoint.ru; +# listen 80; +# return 301 $scheme://cadpoint.ru$request_uri; # Всегда редиректим на основной домен +# } \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..474fb1e --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,148 @@ +# ============================================================================== +# Docker Compose для PRODUCTION +# Этот файл запускается на боевом сервере. +# Вариант 1 (если переименовали в docker-compose.yml): docker compose up -d +# Вариант 2 (если оставили имя): docker compose -f docker-compose.prod.yml up -d +# ============================================================================== + +# В новой версии Docker не нужно +# version: '3.8' + +services: + # --- ОCНОВНОЙ СЕРВИС: DJANGO + GUNICORN + WHITENOISE --- + web: + # Имя контейнера + container_name: cadpoint-backend + + # 1. ОБРАЗ + # В продакшене мы используем готовый, собранный образ из реестра (Gitea) + image: git.cube2.ru/erjemin/2021-cadpoint-ru:latest + # Если образа в gitae нет, то перенести весь код в прод и можно собирать локально: + # build: . + + restart: always + + # 2. Метки для Watchtower (авто-обновление) + labels: + - "com.centurylinklabs.watchtower.scope=cadpoint-scope" + + # 3. КОМАНДА ЗАПУСКА (Замена entrypoint.sh) + # Выполняем цепочку команд внутри контейнера при запуске: + # a. Миграции + # b. Collectstatic + # с. Создаем папку nginx в примонтированном томе конфигов (если нет) + # d. Копирование конфига Nginx с авто-заменой путей через sed (замену реального пути на хосте получаем + # через переменную окружения HOST_PROJECT_PATH) + # e. Инициализация боевого конфига (если нет) + # f. Создаем папку для ошибок и копируем туда HTML error pages и их ассеты (там их увидит Nginx хоста) + # — иконки, SVG-иллюстрации и under_reconstruction тоже должны лежать рядом, чтобы Nginx мог их отдать + # g. Запуск Gunicorn + command: > + sh -c "python manage.py migrate --noinput && + python manage.py collectstatic --noinput --clear && + mkdir -p /nginx_configs_host/nginx && + sed \"s|/home/user/app/cadpoint-site|${HOST_PROJECT_PATH:-/home/default_user/projects/cadpoint-site}|g\" /nginx_configs_host/nginx/cadpoint-app--external-nginx.conf > /nginx_configs_host/nginx/nginx_cadpoint.conf.example && + if [ ! -f /nginx_configs_host/nginx/cadpoint-app--external-nginx.conf ]; then + cp /nginx_configs_host/nginx/nginx_cadpoint.conf.example /nginx_configs_host/nginx/cadpoint-app--external-nginx.conf; + echo 'INIT: Created new nginx config with correct paths'; + fi && + ERROR_DIR=/home/app/web/public/media/_error && + mkdir -p "$$ERROR_DIR/svgs" "$$ERROR_DIR/img" && + for code in 400 401 403 404 413 429 500 502 503 504; do + cp /home/app/web/cadpoint/templates/$${code}.html "$$ERROR_DIR/$${code}.html"; + done && + cp /home/app/web/cadpoint/templates/under_reconstruction.html "$$ERROR_DIR/under_reconstruction.html" && + cp /home/app/web/public/static/svgs/favicon.svg "$$ERROR_DIR/svgs/favicon.svg" && + cp /home/app/web/public/static/svgs/xxx-error.svg "$$ERROR_DIR/svgs/xxx-error.svg" && + cp /home/app/web/public/static/svgs/404-error.svg "$$ERROR_DIR/svgs/404-error.svg" && + cp /home/app/web/public/static/svgs/500-error.svg "$$ERROR_DIR/svgs/500-error.svg" && + cp /home/app/web/public/static/svgs/cappoint_under_reconstruction.svg "$$ERROR_DIR/svgs/cappoint_under_reconstruction.svg" && + cp /home/app/web/public/static/img/favicon.png "$$ERROR_DIR/img/favicon.png" && + cp /home/app/web/public/static/img/favicon.ico "$$ERROR_DIR/img/favicon.ico" && + gunicorn --workers 2 --bind 0.0.0.0:8000 cadpoint.wsgi:application" + + # 4. Проброс портов (Внешний Nginx -> localhost:8050) + ports: + # Слушаем только на localhost хоста, чтобы закрыть прямой доступ из интернета к Gunicorn + - "127.0.0.1:8050:8000" + + # 5. Тома (Volumes) + volumes: + # База данных + # Монтируем папку database с хоста в папку с базой внутри контейнера. + # Путь в контейнере: /home/app/web/database (так как Django ищет базу в BASE_DIR.parent/database) + - ./database:/home/app/web/database + + # Медиа, служебные error-pages и их ассеты лежат в папке `media` на хосте. + - ./media:/home/app/web/public/media + + # Конфиги (Монтируем папку ./config с хоста в /nginx_configs_host внутри контейнера) + # Это нужно, чтобы скрипт запуска мог положить туда .example конфиг и прочитать боевой конфиг. + - ./config:/nginx_configs_host + + # 6. Пользователь и права + user: "1000:1000" + + # Когда нужна отладка процессов внутри контейнера, можно временно раскомментировать эту строку и запустить контейнер с правами root. + # cap_add: + # - SYS_PTRACE + + # 7. Переменные окружения + env_file: + - .env + environment: + - DJANGO_SETTINGS_MODULE=cadpoint.settings + - PYTHONUNBUFFERED=1 + # Передаем переменную с путем на хосте внутрь контейнера, чтобы sed мог её использовать + - HOST_PROJECT_PATH=${HOST_PROJECT_PATH:-/home/default_user/projects/cadpoint-site} + + # 8. Проверка здоровья контейнера (Healthcheck) + # Docker будет периодически проверять статус контейнера. Это критично для Watchtower! + # Если контейнер объявлен "unhealthy", Watchtower сначала остановит старый образ, потом запустит новый. + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/').read()"] + interval: 3m # Проверка каждые 3 минуты + timeout: 12s # Таймаут ответа - 12 секунды + start_period: 20s # Даем 20 секунд на стартап перед первой проверкой + retries: 3 # Unhealthy после 3 неудачных попыток + + # 9. Логирование (Ротация) + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # 10. Ресурсы + deploy: + resources: + limits: + cpus: '0.50' + memory: 512M + mem_limit: 512m + + # --- WATCHTOWER: АВТО-ОБНОВЛЕНИЕ ОБРАЗОВ --- + # Следит за реестром Gitea и обновляет контейнер web, если появился новый image + watchtower: + image: containrrr/watchtower + container_name: cadpoint_watchtower + restart: always + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + # Токен/Логин для вашего приватного реестра (нужно добавить в .env!) + # REPO_USER и REPO_PASS должны быть в .env файле на сервере + - REPO_USER=${REPO_USER} + - REPO_PASS=${REPO_PASS} + - WATCHTOWER_SCOPE=cadpoint-scope + - WATCHTOWER_CLEANUP=true # Удалять старые образы после обновления + - DOCKER_API_VERSION=1.44 + # Дополнительные опции для правильной работы с healthcheck + - WATCHTOWER_WAIT_ON_TIMEOUT=60 # Ждем 60 сек пока контейнер станет healthy перед финализацией + - WATCHTOWER_LIFECYCLE_HOOKS=true # Включаем lifecycle hooks для graceful shutdown + command: --interval 1800 --cleanup # Проверять каждые 30 минут + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" \ No newline at end of file From cd8921c4595f1072b1a481e9832733f096aa3f9a Mon Sep 17 00:00:00 2001 From: erjemin Date: Tue, 14 Apr 2026 17:03:28 +0300 Subject: [PATCH 10/16] =?UTF-8?q?fix:=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20workflow=20=D0=BF=D1=83=D0=B1=D0=BB=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=D1=86=D0=B8=D0=B8=20Docker-=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B7=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/docker-publish.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/docker-publish.yaml b/.gitea/workflows/docker-publish.yaml index 0422340..6bc79bf 100644 --- a/.gitea/workflows/docker-publish.yaml +++ b/.gitea/workflows/docker-publish.yaml @@ -38,8 +38,11 @@ jobs: uses: docker/login-action@v2 with: registry: ${{ env.REGISTRY }} - username: ${{ secrets.REPO_USER }} - password: ${{ secrets.REPO_PASS }} + # Сначала пробуем репозиторные секреты REPO_USER/REPO_PASS, + # а если их нет — используем встроенную пару actor + GITHUB_TOKEN. + # Это защищает workflow от пустых значений в Gitea Actions. + username: ${{ secrets.REPO_USER || github.actor }} + password: ${{ secrets.REPO_PASS || secrets.GITHUB_TOKEN }} # Извлечение метаданных (тегов и лейблов) для Docker - name: Extract metadata (tags, labels) for Docker From 72b0a4145b98012e3a21cd162fc0674f71f4436b Mon Sep 17 00:00:00 2001 From: erjemin Date: Tue, 14 Apr 2026 17:10:26 +0300 Subject: [PATCH 11/16] =?UTF-8?q?fix:=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20workflow=20=D0=BF=D1=83=D0=B1=D0=BB=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=D1=86=D0=B8=D0=B8=20Docker-=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B7=D0=B0=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/docker-publish.yaml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/docker-publish.yaml b/.gitea/workflows/docker-publish.yaml index 6bc79bf..959e865 100644 --- a/.gitea/workflows/docker-publish.yaml +++ b/.gitea/workflows/docker-publish.yaml @@ -38,11 +38,8 @@ jobs: uses: docker/login-action@v2 with: registry: ${{ env.REGISTRY }} - # Сначала пробуем репозиторные секреты REPO_USER/REPO_PASS, - # а если их нет — используем встроенную пару actor + GITHUB_TOKEN. - # Это защищает workflow от пустых значений в Gitea Actions. - username: ${{ secrets.REPO_USER || github.actor }} - password: ${{ secrets.REPO_PASS || secrets.GITHUB_TOKEN }} + username: ${{ github.actor }} + password: ${{ secrets.REGISTRY_PASSWORD }} # Извлечение метаданных (тегов и лейблов) для Docker - name: Extract metadata (tags, labels) for Docker From 7b458b6723dd3706fd56bdfe67c59230ebd17998 Mon Sep 17 00:00:00 2001 From: erjemin Date: Tue, 14 Apr 2026 17:45:03 +0300 Subject: [PATCH 12/16] =?UTF-8?q?fix:=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20workflow=20=D0=BF=D1=83=D0=B1=D0=BB=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=D1=86=D0=B8=D0=B8=20Docker-=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B7=D0=B0=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/docker-publish.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/docker-publish.yaml b/.gitea/workflows/docker-publish.yaml index 959e865..cc494c9 100644 --- a/.gitea/workflows/docker-publish.yaml +++ b/.gitea/workflows/docker-publish.yaml @@ -69,4 +69,4 @@ jobs: # ДОБАВЛЕНО для медленного интернета и оптимизации сборки: cache-from: type=gha cache-to: type=gha,mode=max - timeout: 900 # 15 минут на всю сборку + timeout: 1800 # Увеличено до 30 минут на всю сборку From 0a92ae7a820292666985d618fd1f7583b9103668 Mon Sep 17 00:00:00 2001 From: erjemin Date: Tue, 14 Apr 2026 20:59:29 +0300 Subject: [PATCH 13/16] fix: sync docker-compose.local.yml and docker-compose.prod.yml --- docker-compose.local.yml | 2 +- docker-compose.prod.yml | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docker-compose.local.yml b/docker-compose.local.yml index d4ce512..4bf5e2f 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -23,7 +23,7 @@ services: # Но миграции оставляем, чтобы база была актуальной. command: > sh -c "python manage.py migrate --noinput && - gunicorn --workers 1 --bind 0.0.0.0:8000 --reload cadpoint.wsgi:application" + python -m gunicorn --workers 1 --bind 0.0.0.0:8000 --reload cadpoint.wsgi:application" # 2. МОНТИРОВАНИЕ КОДА (Live Reload) # Подключаем локальные папки внутрь контейнера, чтобы Gunicorn видел изменения без пересборки образа. diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 474fb1e..b7f4312 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -41,7 +41,7 @@ services: sh -c "python manage.py migrate --noinput && python manage.py collectstatic --noinput --clear && mkdir -p /nginx_configs_host/nginx && - sed \"s|/home/user/app/cadpoint-site|${HOST_PROJECT_PATH:-/home/default_user/projects/cadpoint-site}|g\" /nginx_configs_host/nginx/cadpoint-app--external-nginx.conf > /nginx_configs_host/nginx/nginx_cadpoint.conf.example && + 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'; @@ -59,7 +59,7 @@ services: cp /home/app/web/public/static/svgs/cappoint_under_reconstruction.svg "$$ERROR_DIR/svgs/cappoint_under_reconstruction.svg" && cp /home/app/web/public/static/img/favicon.png "$$ERROR_DIR/img/favicon.png" && cp /home/app/web/public/static/img/favicon.ico "$$ERROR_DIR/img/favicon.ico" && - gunicorn --workers 2 --bind 0.0.0.0:8000 cadpoint.wsgi:application" + python -m gunicorn --workers 2 --bind 0.0.0.0:8000 cadpoint.wsgi:application" # 4. Проброс портов (Внешний Nginx -> localhost:8050) ports: @@ -81,7 +81,10 @@ services: - ./config:/nginx_configs_host # 6. Пользователь и права - user: "1000:1000" + # На первом старте контейнеру нужны права на инициализацию bind-mount'ов + # (media/config/database) на хосте, иначе `mkdir` для `media/_error` падает с Permission denied. + # После первичной инициализации можно будет отдельно ужесточить права на уровне хоста. + user: "0:0" # Когда нужна отладка процессов внутри контейнера, можно временно раскомментировать эту строку и запустить контейнер с правами root. # cap_add: From af25b3cb2f973dd03234366ee738c1b4637b59e9 Mon Sep 17 00:00:00 2001 From: erjemin Date: Tue, 14 Apr 2026 21:28:35 +0300 Subject: [PATCH 14/16] mod: update README and remove legacy deploy docs --- README.md | 233 ++++-- deploy_to_dreamhost.md | 189 ----- deploy_to_masterhost-vm.md | 1588 ------------------------------------ 3 files changed, 165 insertions(+), 1845 deletions(-) delete mode 100644 deploy_to_dreamhost.md delete mode 100644 deploy_to_masterhost-vm.md diff --git a/README.md b/README.md index cfff6e0..ec5f87f 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,188 @@ -# Сайт CADpoint.ru +# CADpoint.ru -Сайт с новостями (блог о 3D-печать и Систем Автоматизированного Проектирования) на Django со -встроенными свистелками-перделками: -* медиа-библиотека (filer); -* HTML-редактор на обычной textarea в админке; -* типограф [etpgrf](https://typograph.cube2.ru/); -* теги новостей (taggit). +Сайт CADpoint.ru — это Django-проект, который сейчас живёт в Docker. -[Инструкция по развертыванию на хостинге DreamHost.com](deploy_to_dreamhost.md) +Кратко о схеме: -Для локальной и продовой настройки используй файл `.env` в корне проекта. -Шаблон для него лежит в `.env.sample`. +- сам Django запускается в контейнере `cadpoint-backend`; +- локально используется `docker-compose.local.yml`; +- на проде используется `docker-compose.prod.yml`; +- внешний `nginx` стоит на хосте и проксирует запросы в контейнер; +- конфигурация и секреты лежат в `.env`; +- медиа, статика и SQLite-база живут в проектных каталогах и монтируются в контейнер. -Набор базовых переменных: +## Что есть в проекте -* `DJANGO_SECRET_KEY` -* `DJANGO_DEBUG` -* `DJANGO_ALLOWED_HOSTS` -* `DJANGO_ADMINS` -* `DJANGO_CSRF_TRUSTED_ORIGINS` -* `DJANGO_INTERNAL_IPS` -* `DJANGO_SQLITE_NAME` -* `ADMIN_URL` -* `DJANGO_EMAIL_HOST` -* `DJANGO_EMAIL_PORT` -* `DJANGO_EMAIL_HOST_USER` -* `DJANGO_EMAIL_HOST_PASSWORD` -* `DJANGO_EMAIL_FROM` +- `cadpoint/` — Django-проект, приложения, шаблоны, management commands. +- `public/static/` — исходная статика проекта. +- `public/media/` — загруженные файлы и служебные error-assets. +- `database/` — SQLite-база и дампы. +- `config/nginx/` — конфиг внешнего `nginx` для прод-хоста. +- `frontend-assembly/` — сборка CodeMirror 6 для админки. +- `Dockerfile` — финальный образ приложения. +- `docker-compose.local.yml` — локальный запуск. +- `docker-compose.prod.yml` — prod-запуск. -Для логического бэкапа базы через Django используй команду: +## Быстрый старт + +### 1. Подготовить `.env` + +Скопируй шаблон и заполни значения: ```bash -cd cadpoint -python manage.py backup_db -``` - -По умолчанию файл дампа сохраняется в `database/backups/`. Восстановление делается обычной командой -`python manage.py loaddata .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 -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`, собирает -минимизированный бандл и затем сам удаляет временные `src/` и `node_modules/`. -В проекте остаётся только готовая статика: +- `DJANGO_DEBUG` управляет самим Django. +- `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 .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 ./frontend-assembly/build-codemirror6.sh ``` +Результат сборки — только готовый бандл: + +```text +public/static/codemirror/editor.js +``` + +## Заметки по развертыванию + +- Главный источник правды по запуску — `docker-compose.local.yml` и `docker-compose.prod.yml`. +- Секреты не храним в репозитории: используем `.env`. + diff --git a/deploy_to_dreamhost.md b/deploy_to_dreamhost.md deleted file mode 100644 index 79155c3..0000000 --- a/deploy_to_dreamhost.md +++ /dev/null @@ -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 -Контакт для уведомлений: - -``` -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 установлена в папку `~/` (`/home//opt/python-3.8.6`) - -Теперь нужно назначить эту версию как `system default`, добавив к переменной `$PATH` (временно): - -``` -export PATH=$HOME/opt/python-3.8.6/bin:$PATH -``` - ------------------------------- -_Также можно добавить эту строку в файл `.bashrc` и/или `.bash_profile` в домашней директории `/home/`. Это нужно, чтобы сделать так, чтобы этот 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__ (без создания нового пользователя). В нашем домашнем каталоге будет создана папка `~/`. В этой папке будет лежать `passenger_wsgi.py`, также есть папка `public` в которой будут лежать статичные файлы не требующие обработки CGI (media, static и пр.) - -Теперь создадим виртуальное окружение в папке нашего сайта (`$HOME/`): -``` -virtualenv -p python3 $HOME//env -``` - -Активируем созданное виртуальное окружение: -``` -source $HOME//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 ~//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///env/bin/python3 - -import sys, os -INTERP = "/home///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__ diff --git a/deploy_to_masterhost-vm.md b/deploy_to_masterhost-vm.md deleted file mode 100644 index 7717b05..0000000 --- a/deploy_to_masterhost-vm.md +++ /dev/null @@ -1,1588 +0,0 @@ -# Инструкция по развёртыванию на VDS-хостинге (_виртуальная машина_) на примере masterhost.ru - -## 1: Создание пользователя - -Изначально есть только root-доступ. Если мы залогированы под **root**, то следует создать пользователя от имени -которого мы будем осуществлять все действия (позже root-доступ будет закрыт). Создадим пользователя ****: - -```shell -sudo useradd -c 'WEB-user' -m -``` - -Присвоим ему пароль: -```shell -sudo passwd -``` - -Дадим ему права работать от имени root. Для этого откроим на редактирование конфигурационный файл `/etc/sudoers`: -```shell -nano /etc/sudoers -``` - -и после строки `root ALL=(ALL:ALL) ALL` добавим в него строку: -```editorconfig - ALL=(ALL:ALL) ALL -``` - -Сохраняем конфигурационный файл `Ctrl+O` и `Enter`, а выходим из редактора `Ctrl+X`. - -**ВАЖНО**: Если по какой-то причине файл `/etc/sudoers` пустой -- это означает, что служба sudoers не установлена. -Её нужно установить: `apt-get install sudo`. - -Разлогируемся оз под root. -```shell -logout -``` - -Теперь можно залогироваться от имени пользователя ****. - -Установим командную оболочку bash для пользователя: -```shell -chsh -s /bin/bash -``` - -## 2: Настройка SSH и окружения клиента - -### Ограничиваем доступ root и повышение безопасности SSH - -После завершения перезагрузки произведённой в конце предыдущего этапа мы можем заходить на наш сервер по SSH под новым, только что созданным, пользователем [user] и паролем [user_pwd]. Теперь следует сделать небольшие изменения в файле `/etc/ssh/sshd_config` -- конфигурации ssh-доступа: - -```shell -sudo nano /etc/ssh/sshd_config -``` - -Полное описание настроек sshd_config [находится на сайте Ubuntu](https://help.ubuntu.ru/wiki/ssh). Там содержатся -вполне разумные рекомендации по увеличению безопасности. Откроем на редактирование конфигурационный файл ` -/etc/ssh/sshd_config`: -```shell -sudo nano /etc/ssh/sshd_config -``` - -И изменим порт на котором будет отвечать SSH: -```editorconfig -Port 2002 -``` - -Так же дописываем в конце следующие две строки в которых и запрещаем ssh-вход -пользователя **root** и разрешаем доступ нашему пользователю ****: -``` -DenyUsers root -AllowUsers -``` - -Сохраняем конфигурационный файл `Ctrl+O` и `Enter`, а выходим из редактора `Ctrl+X`. - -Чтобы настройки подействовали нужно перезапустить ssh-сервис: -```shell -sudo service ssh restart -``` - -Проверим, что сервис корректно перезапустился: -```shell -sudo service sshd status -``` - -Увидим, что все работает: -``` -● ssh.service - OpenBSD Secure Shell server - Loaded: loaded (/lib/systemd/system/ssh.service; enabled; vendor preset: enabled) - Active: active (running) since Wed 2022-05-25 01:01:24 MSK; 2 days ago - Docs: man:sshd(8) - man:sshd_config(5) - Main PID: 647 (sshd) - Tasks: 1 (limit: 1066) - Memory: 5.8M - CGroup: /system.slice/ssh.service - └─647 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups -``` - -Разлогируемся -- `logout`. Входим ещё раз и можем проверяем, что теперь пользователя root больше в систему по SSH не пускают. Логируется вновь созданным пользователем [user]. - -### Упрощение идентификации сервера при входе - -В конфигурационном файле `/etc/ssh/sshd_config есть` специальный параметр `Banner` в котором указывается файл, который следует показать при входе (обычно `/etc/ssh_banner`). Его отображение упрощает идентификацию сервера при входе, его сложнее спутать с другими, особенно когда работа идёт одновременно с терминалами нескольких серверов. Но он этот баннер отображается только при входе через SSH. Существует более радикальное решение, отображающееся и при обычном входе. Оно упростит идентификацию даже если мы работаем с консоли (например, в случае нескольких виртуальных серверов на одном физическом). - -Очистим файл `/etc/motd`, и поместим туда какой-нибудь красивый ASCII-арт текст из ASCII-генератора. : -```shell -sudo nano /etc/motd -``` - -Например, вот так: -``` -███████╗░░░░░░██████╗███████╗██████╗░░██████╗░ MASTERHOST.RU HOSTED: -██╔════╝░░░░░██╔════╝██╔════╝██╔══██╗██╔════╝░ -█████╗░░████╗╚█████╗░█████╗░░██████╔╝██║░░██╗░ cadpoint.ru -██╔══╝░░╚═══╝░╚═══██╗██╔══╝░░██╔══██╗██║░░╚██╗ oknardia.ru -███████╗░░░░░██████╔╝███████╗██║░░██║╚██████╔╝ cube2.ru -╚══════╝░░░░░╚═════╝░╚══════╝╚═╝░░╚═╝░╚═════╝░ venturebox.org -``` -Сохраняем баннер `Ctrl+O` и `Enter`, а выходим из редактора `Ctrl+X`. - -### Раскрашиваем оболочку bash - -Чтобы сделать красочно-разноцветным нашу командную строку, откроем на редактирование файл настроек оболочки bash пользователя `.bashrc`: -```shell -nano ~/.bashrc -``` -находим там строку `#force_color_prompt=yes` раскомментируем её (и удаляем в ней #). И чтобы совсем отпад, находим блок: - -``` -if [ "$color_prompt" = yes ]; then - PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ ' -else - PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ ' -fi -``` - -И меняем на блок -``` -if [ "$color_prompt" = yes ]; then - PS1='${debian_chroot:+($debian_chroot)}\[\033[01;31m\]\u\[\033[01;34m\]@\[\033[01;31m\]\h\[\033[00m\]:\[\033[00;34m\]\w\[\033[00m\]\> -else - PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ ' -fi -``` - -Всё! Перелогиниваемся чтобы настройки подействовали. - -```shell -logout -``` - -## 3. Настраиваем службу времени (необязательно) - -Устанавливаем службу точного времени nlp: -```shell -apt-get install ntp -``` - - Откроем на редактирование конфигурационный файл `/etc/ntp.conf`: -```shell -sudo nano /etc/ntp.conf -``` - -Заменяем буржуйские адреса серверов точного времени на отечественные: -```editorconfig -# Specify one or more NTP servers. - -# Use servers from the NTP Pool Project. Approved by Ubuntu Technical Board -# on 2011-02-08 (LP: #104525). See http://www.pool.ntp.org/join.html for -# more information. -pool ntp.msk-ix.ru iburst prefer -pool 194.190.168.1 iburst -pool 2001:6d0:ffd4::1 -# pool ntp.ix.ru -# pool 1.ubuntu.pool.ntp.org iburst -# pool 2.ubuntu.pool.ntp.org iburst -# pool 3.ubuntu.pool.ntp.org iburst - -# Use Ubuntu's ntp server as a fallback. -pool ntp.msk-ix.ru -``` - -Сохраняем баннер `Ctrl+O` и `Enter`, а выходим из редактора `Ctrl+X`. - -Чтобы настройки подействовали сервис точного времени нужно перезапустить: -```shell -sudo service ntp restart -``` - -Проверим, что сервис корректно перезапустился: -```shell -sudo service ntp status -``` - -Увидим, что все ок: -``` -● ntp.service - Network Time Service - Loaded: loaded (/lib/systemd/system/ntp.service; enabled; vendor preset: enabled) - Active: active (running) since Wed 2022-05-25 01:01:24 MSK; 2 days ago - Docs: man:ntpd(8) - Main PID: 642 (ntpd) - Tasks: 2 (limit: 1066) - Memory: 1.5M - CGroup: /system.slice/ntp.service - └─642 /usr/sbin/ntpd -p /var/run/ntpd.pid -g -u 112:119 -``` - -Проверить какой сервер точного времени ближе, задержки между серверами и т.п.: -```shell -ntpq -p -``` - -Увилим что-то типа: -```shell - remote refid st t when poll reach delay offset jitter -============================================================================== - ntp.msk-ix.ru .POOL. 16 p - 64 0 0.000 0.000 0.000 - 194.190.168.1 .POOL. 16 p - 64 0 0.000 0.000 0.000 - 2001:6d0:ffd4:: .POOL. 16 p - 64 0 0.000 0.000 0.000 - ntp.ix.ru .POOL. 16 p - 64 0 0.000 0.000 0.000 -*ntp.ix.ru .GPS. 1 u 450 1024 377 2.442 -0.254 0.117 -``` - -## 4. Настраиваем бренмауэр для использования только с допустимыми внешними интерфейсами (защита портов) - -Устанавливаем iptables для управления IP-соединения и iptables-persistent для сохранения конфигураций настроенных соединений и их автоматического подключения после перегрузке компьютера. -```shell -sudo -S apt-get install iptables -``` - -Далее создадим в домашней папке bash-скрипт `prepare-iptable.sh`: -```shell -nano ~/prepare-iptable.sh -``` - -...и вставим в него нижеследующий текст (исправьте если необходимо, следуя рекомендациям): -```shell -#!/bin/bash - -# You have to set the rules of your firewall on your server only with the -# services used outside the VM. - -# Вы должны установить правила брандмауэра на своем сервере только -# с сервисами, используемыми вне виртуальной машины. - -echo "Сетевые настройки: разрешения трафика и портов"; -echo ""; -echo "ЭТОТ СКРИПТ НУЖНО ЗАПУСКАТЬ С ПРАВАМИ АДМИНИСТРАТОРА (ИЗ ПОД SUDO)."; -echo "ОБАЗАТЕЛЬНО ИЗ KVM-КОНСОЛИ! ИЗ ПОД SSH МОЖЕТ СЛОМАТЬ ВАШУ ВИРТАЛКУ!"; -echo "==================================================================="; -echo ""; -read -p "Хотите чтобы скрипт сделал это (возможно, и сломает)? (Y/N):" -n 1 -r -echo -if [[ ! $REPLY =~ ^[Yy]$ ]] -then - exit 1 -fi - -# Установим пакет iptables (если он уже установлен, попытка повторной установки "проскочит" -sudo -S apt-get install iptables - -# ПРИСВАИВАЕМ ПЕРМЕННЫЕ: -# $IPT = "/usr/sbin/iptables" -# $WAN = текущий внешний сетевой интерфейс виртуалки -# $WAN_IP = текущий IP V4 назначенный на внешний сетевой интерфейс виртуалки -# $WAN_IP6 = текущий IP V6 назначенный на внешний сетевой интерфейс виртуалки -export IPT="$(sudo -S which iptables)" -export WAN="$(ip add | grep 'BROADCAST' | awk '{print $2}' | cut -d ':' -f 1)" -export WAN_IP="$(ip ad | grep 'inet ' | awk '(NR == 2)' | awk '{print $2}' | cut -d '/' -f 1)" -export WAN_IP6="$(ip ad | grep 'inet6 ' | awk '(NR == 2)' | awk '{print $2}' | cut -d '/' -f 1)" - -echo "-------------------ТЕКУЩИЕ ПРАВИЛА--------------------"; -$IPT -L -v -n - -# СБРАСЫВАЕМ старые правила: -$IPT -F -$IPT -X - -# БАЗОВЫЕ правила (политика по умолчанию) -- все обрубаем! -$IPT -P INPUT DROP -$IPT -P OUTPUT DROP -$IPT -P FORWARD DROP - -# Разрешаем локальный траффик для loopback (для localhost) -$IPT -A INPUT -i lo -j ACCEPT -$IPT -A OUTPUT -o lo -j ACCEPT - -# Разрешаем пинги -# $IPT -A INPUT -p icmp --icmp-type echo-reply -j ACCEPT -# $IPT -A INPUT -p icmp --icmp-type destination-unreachable -j ACCEPT -# $IPT -A INPUT -p icmp --icmp-type time-exceeded -j ACCEPT -# $IPT -A INPUT -p icmp --icmp-type echo-request -j ACCEPT - -# Разрешаем исходящие соединения с самого сервера -$IPT -A OUTPUT -o $WAN -j ACCEPT - -# Состояние ESTABLISHED говорит о том, что это не первый пакет в соединении. -# Пропускать все уже инициированные соединения, а также дочерние от них. -# Так мы разрешим использование текущего порта на который настроен и -# открыт, в настощий момент, SSH а еще доступ к репозиториям для получения -# новых пакетов и их обновлений. -$IPT -A INPUT -p all -m state --state ESTABLISHED,RELATED -j ACCEPT -# Разрешить новые, а так же уже инициированные и их дочерние соединения -$IPT -A OUTPUT -p all -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT -# Разрешить форвардинг для уже инициированных и их дочерних соединений -$IPT -A FORWARD -p all -m state --state ESTABLISHED,RELATED -j ACCEPT - -# Включаем фрагментацию пакетов. Необходимо из-за разных значений MTU -$IPT -I FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu - -# Отбрасывать все пакеты, которые не могут быть идентифицированы -# и поэтому не могут иметь определенного статуса. -$IPT -A INPUT -m state --state INVALID -j DROP -$IPT -A FORWARD -m state --state INVALID -j DROP - -# Приводит к связыванию системных ресурсов, так что реальный -# обмен данными становится не возможным, обрубаем -$IPT -A INPUT -p tcp ! --syn -m state --state NEW -j DROP -$IPT -A OUTPUT -p tcp ! --syn -m state --state NEW -j DROP - -# Открываем только нужные порты на вход: -# !!! указать свой порт, который вы указали для SSH ранее !!!) -$IPT -A INPUT -i $WAN -p tcp --dport 2002 -j ACCEPT -# порт для web сервера (для http) -$IPT -A INPUT -i $WAN -p tcp --dport 80 -j ACCEPT -# порт для web сервера (для https) -$IPT -A INPUT -i $WAN -p tcp --dport 443 -j ACCEPT - -## Логирование -## Все что не разрешено, но ломится отправим в цепочку undef -# $IPT -N undef_in -# $IPT -N undef_out -# $IPT -N undef_fw -# $IPT -A INPUT -j undef_in -# $IPT -A OUTPUT -j undef_out -# $IPT -A FORWARD -j undef_fw - -# Логируем все из undef -# $IPT -A undef_in -j LOG --log-level info --log-prefix "-- IN -- DROP " -# $IPT -A undef_in -j DROP -# $IPT -A undef_out -j LOG --log-level info --log-prefix "-- OUT -- DROP " -# $IPT -A undef_out -j DROP -# $IPT -A undef_fw -j LOG --log-level info --log-prefix "-- FW -- DROP " -# $IPT -A undef_fw -j DROP - -# Записываем правила -$IPT-save > $IPT.rules - -# Выводим правила на экран: -echo "----------ПРАВИЛА ДЛЯ TCP-ПОРТОВ УСТАНОВЛЕНЫ----------"; - -cat $IPT.rules - -echo "---------------------И ПРИМЕНЕНЫ----------------------"; - -$IPT -L -v -n - -echo "------------------------------------------------------"; -echo ""; -echo "Правила сохранены в файле: $IPT.rules"; -echo ""; -echo "Для восстановления правил запустите скрипт повторно или"; -echo "исполните команду:"; -echo ""; -echo "sudo $IPT-restore < $IPT.rules"; - -echo ""; -echo ""; -echo " _._ _,-'\"\"\`-._"; -echo "(,-.\`._,'( |\\\`-/|"; -echo " \`-.-' \\ )-\`( , o o)"; -echo " \`- \\\`_\`\"'- Mi-mi-mi... Ok!"; -echo ""; -``` - -Сохраняем скрипт `Ctrl+O` и `Enter`, а выходим из редактора `Ctrl+X`. - -Далее нужно зайти на вирталку через KVM-консоль и выполнить: -```shell -sudo bash ~/prepare-iptable.sh -``` -После выполнения скрипта все внешние соединения с виртуалкой будут разорваны. После перелогирования через ssh можно будет увидеть, что новые настройки сетевой фильтрации вступили в силу: -```shell -sudo iptables -L -v -n -``` - -Увидим примерно следующее: -```text -Chain INPUT (policy DROP 150 packets, 5768 bytes) - pkts bytes target prot opt in out source destination - 28 2204 ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0 - 81 7202 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED - 2 84 DROP all -- * * 0.0.0.0/0 0.0.0.0/0 state INVALID - 8 608 DROP tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp flags:!0x17/0x02 state NEW - 1 52 ACCEPT tcp -- eth0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:2002 - 0 0 ACCEPT tcp -- eth0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 - 0 0 ACCEPT tcp -- eth0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:443 - -Chain FORWARD (policy DROP 0 packets, 0 bytes) - pkts bytes target prot opt in out source destination - 0 0 TCPMSS tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp flags:0x06/0x02 TCPMSS clamp to PMTU - 0 0 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED - 0 0 DROP all -- * * 0.0.0.0/0 0.0.0.0/0 state INVALID - -Chain OUTPUT (policy DROP 0 packets, 0 bytes) - pkts bytes target prot opt in out source destination - 28 2204 ACCEPT all -- * lo 0.0.0.0/0 0.0.0.0/0 - 69 10113 ACCEPT all -- * eth0 0.0.0.0/0 0.0.0.0/0 - 0 0 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state NEW,RELATED,ESTABLISHED - 0 0 DROP tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp flags:!0x17/0x02 state NEW -``` - -Чтобы сохранить настройки и чтобы они автоматически вступали в силу в -случае перезагрузки установим пакет `iptables-persistent`: -```shell -sudo -S apt-get install iptables-persistent -``` - -Будет запрошено сохранить ли и восстанавливать настройки в случае -перезагрузки для IP-V4 и IP-V6 -- оба раза отвечаем `Y`. - -В будущем, если будут меняться правила фильтрации, то сохранить `sudo -netfilter-persistent save`, восстановить `sudo netfilter-persistent -start`. Для детальных разъяснений [см. по -ссылке](https://losst.ru/kak-sohranit-pravila-iptables). - -## 5. Установиv защиту DoS (брутфорса) по ssh - -DoS и DDoS-атака -- это агрессивное внешнее воздействие на сервер, проводимое с целью перегрузить его запросами, -доведения его до отказа или брутфорс-взлома (brute force -- грубая сила, например, подбор паролей перебором и т.п.). - -Если атака проводится с одиночного компьютера -- ее называют DoS (Denial of Service), если с нескольких, -распределенных в сети, компьютеров — DDoS (Distributed Denial of Service). - -Защиту от DoS (включая медленные атаки) может обеспечить пакет fail2ban. - -Установим его (уже должен быть установлен): -````shell -sudo apt-get install fail2ban -```` - -Для его настройки перепишем конфигурационный файл `/etc/fail2ban/jail.local`: -```shell -sudo nano /etc/fail2ban/jail.local -``` -И поместим туда следующее: -```editorconfig -# == новый конфиг /etc/fail2ban/jail.local ===================== -[DEFAULT] -#email, на который присылать уведомления -destemail = root -# подключить прафила бана из '/etc/fail2ban/action.d/iptables-multiport.conf' -banaction = iptables-multiport -# исключаем из потенциального бана ip машины с которых можем подключаться -ignoreip = - -#### правила для SSH #### -[sshd] -enabled = true -port = ssh,2002 -# filter — подключить правила фильтрации из '/etc/fail2ban/filter.d/sshd.conf' -filter = sshd -# logpath — какой лог наблюдаем (на тот случай, если он не по умолчанию) -logpath = /var/log/auth.log -# bantime — время (секунды) на которое баним. На неделю — 60*60*24*7=604800 -bantime = 604800 -# maxretry — число попыток (1) и получаешь бан! -maxretry = 1 -# findtime — определяет длительность интервала в секундах, за которое -# событие должно повториться определённое количество раз, после чего санкции -# вступят в силу. Если специально не определить этот параметр, то будет -# установлено значение по умолчанию равное 600 (10 минут). Проблема в том, -# что ботнеты, участвующие в «медленном брутфорсе», умеют обманывать -# стандартное значение. Иначе говоря, при maxretry равным 6, -# атакующий может проверить 5 паролей, затем выждать 10 минут, -# проверить ещё 5 паролей, повторять это снова и снова, и его IP забанен -# не будет. В целом, это не угроза, но всё же лучше банить таких ботов -findtime = 7200 - -# СТАРЫЙ КОНФИГ ПО УМОЛЧАНИЮ -# [DEFAULT] -# bantime = 600 -# findtime = 60 -# maxretry = 6 -# banaction = iptables-multiport -# [sshd] -# enabled = true -``` -Сохраняем конфигурационный файл `Ctrl+O` и `Enter`, а выходим из редактора `Ctrl+X`. - -Перезапустим fail2ban: -```shell -sudo service fail2ban restart -``` - -Проверим статус: -```shell -sudo service fail2ban status -``` - -Увидим, что все работает: -``` -● fail2ban.service - Fail2Ban Service - Loaded: loaded (/lib/systemd/system/fail2ban.service; enabled; vendor preset: enabled) - Active: active (running) since Tue 2022-05-17 14:33:22 MSK; 1h 53min ago - Docs: man:fail2ban(1) - Process: 7635 ExecStartPre=/bin/mkdir -p /run/fail2ban (code=exited, status=0/SUCCESS) - Main PID: 7640 (f2b/server) - Tasks: 5 (limit: 1066) - Memory: 11.0M - CGroup: /system.slice/fail2ban.service - └─7640 /usr/bin/python3 /usr/bin/fail2ban-server -xf start - -May 17 14:33:22 vm2203242538 systemd[1]: Starting Fail2Ban Service... -May 17 14:33:22 vm2203242538 systemd[1]: Started Fail2Ban Service. -May 17 14:33:22 vm2203242538 fail2ban-server[7640]: Server ready -``` - -Включаем автозапуск fail2ban при заугрузке системы: -```shell -sudo systemctl enable fail2ban.service -``` - -## 5. Установка и настройка СУБД (MariaDB), создание пользователей - -Следуем [инструкциям](https://www.digitalocean.com/community/tutorials/how-to-install-mariadb-on-ubuntu-20-04), и у нас получится примерно такой порядок действий: - -### Установка - -Устанавливаем сервер MariaDB (полный аналог MySQL, но без патронажа Oracle) и пакет MariaDB-клиента для -разработчиков (чтобы после корректно установился MySQL-коннектор для Python): -```shell -sudo apt install mariadb-server libmysqlclient-dev -``` - -Отредактируем конфигурационный файл, чтобы сменить кодовые таблицы в будущих базах с `utf8mb4` (со всякими глупыми -эможи) на православную `utf8`: -```shell -sudo nano /etc/mysql/mariadb.conf.d/50-server.cnf -``` - -Найдём в конфигурационном файле строки: -```editorconfig -character-set-server = utf8mb4 -collation-server = utf8mb4_general_ci -``` - -и заменим на: -```nginx configuration -character-set-server = utf8 -collation-server = utf8_general_ci -``` -Сохраняем конфигурационный файл `Ctrl+O` и `Enter`, а выходим из редактора `Ctrl+X`. - -Перезапустим систему управления базы данных: -```shell -sudo service mysql restart -``` - -Проверим статус: -```shell -sudo service mysql status -``` - -Если все сделали правильно, должны увидеть что-то типа: -``` -● mariadb.service - MariaDB 10.6.7 database server - Loaded: loaded (/lib/systemd/system/mariadb.service; enabled; vendor preset: enabled) - Active: active (running) since Tue 2022-08-09 09:25:26 MSK; 3 days ago - Docs: man:mariadbd(8) - https://mariadb.com/kb/en/library/systemd/ - Main PID: 1249 (mariadbd) - Status: "Taking your SQL requests now..." - Tasks: 9 (limit: 935) - Memory: 82.5M - CPU: 13min 42.376s - CGroup: /system.slice/mariadb.service - └─1249 /usr/sbin/mariadbd -``` - -### Создание пользователей - -Для этого запустим mysql с супер-правами `root`: -```shell -sudo mysql -``` - -Создаём пользователя ``, зададим ему пароль и дадим привилегии на все: -```mysql -CREATE USER ''@'localhost' IDENTIFIED BY '*********************'; -GRANT ALL PRIVILEGES ON *.* TO ''@'localhost'; -``` - -Создаем базу данных `django_cadpoint` для нашего сайта: -```mysql -CREATE DATABASE django_cadpoint DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci; -``` - -И создадим пользователя `cadpint`, пароль для него и дадим ему права работать только с базой сайта: -```mysql -CREATE USER 'cadpint'@'localhost' IDENTIFIED BY '*******'; -GRANT ALL PRIVILEGES ON django_cadpoint.* TO 'cadpint'@'localhost'; -``` - -Проверим, что установлена правильная часовая зона времени: -```mysql -SHOW VARIABLES LIKE '%time_zone%'; -``` - -Типа, она должна быть `MSK`: -``` -+------------------+--------+ -| Variable_name | Value | -+------------------+--------+ -| system_time_zone | MSK | -| time_zone | SYSTEM | -+------------------+--------+ -2 rows in set (0.001 sec) -``` - -Выходим из MariaDB: -```mysql -quit; -``` - -переносим базу - -## 6. ПОДГОТОВКА WEB-СЕРВЕРА NGINX - -Устанавливаем nginx -```shell -sudo apt-get install nginx -``` - -На всякий случай (как выяснилось позже), лучше поставить nginx plus. В нем есть динамические модули. И один их -таких динамических модулей -- **geoip** -- может понадобиться для ограничения посещения сайта только пользователями -России. [Смотрим инструкцию по установке nginx plus](https://docs.nginx.com/nginx/admin-guide/installing-nginx/installing-nginx-plus/). - -Убедиться, что он запущен и работает корректно можно командой: -```shell -sudo service nginx status -``` - -Результат должен быть примерно таким: -``` -● nginx.service - A high performance web server and a reverse proxy server - Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled) - Active: active (running) since Thu 2022-01-20 18:45:16 MSK; 56s ago - Docs: man:nginx(8) - Main PID: 2200 (nginx) - Tasks: 2 (limit: 1037) - Memory: 4.6M - CGroup: /system.slice/nginx.service - ├─2200 nginx: master process /usr/sbin/nginx -g daemon on; master_process on; - └─2201 nginx: worker process -``` - -Зайдем по ip нашего сервера или через localhost: -```shell -curl -I http://localhost/ -``` - -Получим ответ: -``` -HTTP/1.1 200 OK -Server: nginx/1.18.0 (Ubuntu) -Date: Tue, 03 May 2022 18:16:46 GMT -Content-Type: text/html -Content-Length: 612 -Last-Modified: Tue, 03 May 2022 12:49:44 GMT -Connection: keep-alive -Vary: Accept-Encoding -ETag: "627124e8-264" -Accept-Ranges: bytes -``` - -### Настройка GZIP - -По умолчанию модуль GZIP установлен в NGINX. Он осуществляет сжатие данных -«на лету», они отправляются nginx, и, после получения, распаковываются -браузерами (само-собой теми, которые поддерживают такую возможность). При -этом между веб-сервером и браузером передается меньшее данных, ускоряя тем самым . Сжатие -использует ресурсы сервера, поэтому лучше всего сжимать только те файлы, -которые хорощо «сжимаютсяу» (а еще лучще, есди еще и могут кэшироваться на -стороне браузера). Текстовые файлы сжимаются хорошо, JPEG или PNG уже -сжаты по своей природе и большого результата при сжатии GZIP можно не -ожидать. - -Настроим модуль GZIP в NGINX. Для этого открываем на редактирование -конфигурационный файл nginx `/etc/nginx/nginx.conf`: -```shell -sudo nano /etc/nginx/nginx.conf -``` - -Находим в нем кусочек касающийся GZIP: -```nginx configuration - ## - # Gzip Settings - ## -``` - -и пишем в нем следующее (там будет закомментированный блок, можно -использовать его): -```nginx configuration - gzip_proxied any; - gzip_comp_level 6; - gzip_buffers 16 8k; - gzip_http_version 1.1; - gzip_disable "msie6"; - gzip_vary on; - gzip_min_length 512; - gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon; -``` - -Если интересно, расшифруем что тут происходит: -* _gzip_proxied any_ - сжимать данные ответов для proxy-серверов. -* _gzip_comp_level 6_ - устанавливаем, степень сжатия файлов. Чем выше -число, тем выше уровень сжатия и использование ресурсов. уровень сжатия, 1 -- минимальное, 9 - максимальное. -* _gzip_buffers 16 8k_ - задаёт число и размер буферов, в которые будет -сжиматься ответ. По умолчанию размер одного буфера равен размеру страницы. -В зависимости от платформы это или 4K, или 8K. -* _gzip_http_version 1.1_ - директива используется для ограничения сжатия -gzip для браузеров, поддерживающих протокол HTTP/1.1. Если браузер не -поддерживает его, вероятно, что он не поддерживает и gzip. -* _gzip_disable "msie6"_ - исключаем IE6 из браузеров, которые будут -получать сжатые файлы (этот древний браузер не поддерживает GZIP). -* _gzip_vary on_ - включает добавление в ответ заголовка __"Vary: -Accept-Encoding"__ (для IE4-6 это приведёт к не кешированию данных из-за -бага). -* _gzip_min_length 512_ - сообщаем NGINX не сжимать файлы размером менее -512 байт. -* _gzip_types_ - отображает все типы MIME, которые будут сжаты. В этом -случае список включает страницы HTML, таблицы стилей CSS, файлы Javascript -и JSON, файлы XML, иконки (BMP-изображения очень даже сжимаются), -изображения SVG и веб-шрифты. - -Ещё в этом же `/etc/nginx/nginx.conf` нужно заменить дерективу `user www-data;` на `user root;` (в самой первой -строчке... почему-то на Ubuntu 2022.04 по другому не работает uwsgi... похоже не может получить доступ к сокету... -странно...) - -Сохраняем конфигурационый файл `Ctrl+O` и `Enter`, а выходим из редактора -`Ctrl+X` и перезагружаем nginx: -```shell -sudo service nginx restart -``` - -#### Проверка новой конфигурации - -Выполним тестовый запрос: -```shell -curl -H "Accept-Encoding: gzip" -I http://localhost/ -``` - -Мы получим ответ: -``` -HTTP/1.1 200 OK -Server: nginx/1.18.0 (Ubuntu) -Date: Tue, 03 May 2022 18:16:53 GMT -Content-Type: text/html -Last-Modified: Tue, 03 May 2022 12:49:44 GMT -Connection: keep-alive -Vary: Accept-Encoding -ETag: W/"627124e8-264" -Content-Encoding: gzip -``` - -Как видим, сжатие включилось. - -## 7. Развёртывание окружения проекта - -### Настройка виртуального окружения проекта - -Установим python с набором для разработчиков (для сборки коннектора к сСУБД), пакетный менеджер и утилиту для -создания виртуального окружения python: -```shell -sudo apt-get install python3-pip python3-virtualenv python3-dev -``` - -Проверим, что установлена нужная нам версия Python (нам нужно Python 3.8.10): -``` -python3 -V -``` - -Теперь создадим папку для нашего сайта `~/cadpoint` и развернём в ней -виртуальное окружение, указав, что в нем нужно использовать версию Python -3.7 /usr/bin/python3.7. Будет создан каталог, с файлами виртуального -окружение (версия Python, установщик пакетов pip, wheel, setuptools а, в -будущем, и все пакеты, батарейки, свистелки и хрюкалки нашего проекта). -```shell -cd $HOME -mkdir -p cadpoint -``` - -Чтобы для нашего проекта "заморозить" версию Python и все необходимые пакеты, и изьежать возможного кофликта при обновлении системного Python или с пакетвми установленными в других проектвх создадим виртуальное окружение в папке нашего сайта (`$HOME/cadpoint`): -```shell -virtualenv -p python3 $HOME/cadpoint/env -``` - -Активируем созданное виртуальное окружение: -```shell -source $HOME/cadpoint/env/bin/activate -``` - -Проверить, что теперь мы работаем в виртуальном окружении можно дав команды: -```shell -python -V -pip -V -``` -Увидим -> pip 20.0.2 from /home//cadpoint/env/lib/python3.8/site-packages/pip (python 3.8) - -Можно обновить менеджер пакетов `pip` по последней версии (сперва получим его с помощью `curl` в папку tmp, а после запустим установщик через Python) -```shell -curl https://bootstrap.pypa.io/get-pip.py > ~/tmp/get-pip.py -python3 ~/tmp/get-pip.py -``` -Попробуем снова -```shell -pip -V -``` -Увидим -> pip 22.0.4 from /home//cadpoint/env/lib/python3.8/site-packages/pip (python 3.8) - -ФУУ!! - -## 8. Установка пакетов необходимых проекту - -Точный состав пакетов, обычно, находится в файле [requarement_dev_prod_dreamhost.txt](rsvo_new/requarement_dev_prod_nicru_vm.txt). Но на всякий -случай приведем список пакетов здесь (он может отличаться от действительно актуального, в файле) т.к., на самом деле, -большинство пакетов будут установлены автоматически как зависимости: - - -| Пакет | Версия | Назначение | Зависимости | -|------|---------|--------------------------------------------------------------------------------------------------------------------------------|------| -| django | 3.2.15 | Фреймворк Django | притащит с собой пакеты: __asgiref__, __pytz__ и __sqlparse__ -| mysqlclient | 2.1.1 | Коннектор MySQL | нет -| django-filer | 2.2.2 | Система управления медиа-файлами с фишками подготовки ресайз-картинок, превьюшек и прочими плюшками | притащит с собой пакеты: __Unidecode__, __django-js-asset__, __django-mptt__, __django-polymorphic__, __easy-thumbnails__ и __pillow__ -| htmlarea | встроено | Обычная многострочная форма редактирования HTML в админке | нет -| django-taggit | 3.0.0 | Ситема управления тегами | нет -| pytils | 0.4.4 | Пакет рускоязычной транслитерации, работы с числительными, склонениями числительных и временными диаппазонами (для Python 3.x) | нет -| urllib3 | 1.26.11 | пакет для работы с web-запросами (проекту этот пакет нужен для работы с API внешний HTML-типографов) | нет - -Все эти пакеты устанавливаются в виртуальное окружение с помощью пакетного -менеджера `pip` в последовательности: - -```shell -pip install Django==3.2.15 -pip install mysqlclient==2.1.1 -pip install django-filer==2.2.2 -pip install django-taggit==3.0.0 -pip install pytils==0.4.4 -pip install urllib3==1.26.11 -``` - -### Разворачиваем и тестируем проект - -Создаём папки для хранения сокета и логов: -```shell -mkdir -p $HOME/cadpoint/logs -mkdir -p $HOME/cadpoint/socket -``` - -Переносим проект... например на тест-деплоя z7 (или на на бочем проекте мастерхост) архивируем проект. __Само-cобой это -надо делать там, где проект находится, не на вирталке, а dev-серере (или или там находится наш проект)__. Например, -так: -```shell -zip -9 -r cadpoint.zip public/ rsvo_new/ config/ -``` - -Копируем архив по ssh на нашу виртуалку (masterhost или nic.ru) -```shell -scp -P 2002 cadpoint.zip @:~/ -``` - -Возвращаемся на нашу виртуалку хостинга и разархивируем: -```shell -cd ~/cadpoint -unzip cadpoint.zip -``` - -__Внимание__, на разных серверах расположение папок проекта может отличаться. При архивации илм после распаковки, -возможно, нам потребуется переместить некоторые папки. Мы должны получить следующую структуру проекта: -```text -+---public -¦ +---media -¦ L---static -+---rsvo_new -¦ +---rsvo_new -¦ +---templates -¦ L---web -+---logs -+---config -+---env -L---socket -``` - -Папки __logs__, __config__, __env__ и __socket__ на данном этапе могут отсутствовать. Мы создадим их позже: - -__Еще важно__, если у нас установилось Django 4 (а изначально проект создавался для Django 3.2.х) -то нам надо сделать небольшие изменения в коде. - -В файле проекта __urls.py__ (он должен быть расположен тут: `/home//cadpoint/rsvo_new/rsvo_new/urls.py`) -из-за изменения расположения методов в библиотеках django следует закоменить строчку: -```python -from django.conf.urls import url, include -``` - -И добавить после нее: -```python -from django.conf.urls import include -from django.urls import re_path as url -``` - -должно получиться следующее: -```python -# from django.conf.urls import url, include -from django.conf.urls import include -from django.urls import re_path as url -``` - -Редактор в админке теперь обычный, поэтому отдельные правки для стороннего WYSIWYG-пакета больше не нужны. - -Теперь можно произвести перенос статических файлов админки и батареек в папку для web-статики: -```shell -source ~/cadpoint/env/bin/activate -cd ~/cadpoint/rsvo_new -python manage.py collectstatic -``` - -Теперь произведем подготовку нашей базы под проект. Для этого произведем миграции: -```shell -python manage.py makemigrations -python manage.py migrate -``` - -Миграция создаст все необходимые таблицы и заполнит таблицу миграций `django_cadpoint.django_migration`. - -Теперь можно произвести перенос базы с dev-проекта (или с другого рабочего сервера) в базу нашей виртуалки. -Следует учесть, что переносить таблицу `django_cadpoint.django_migration` не следует (мы произвели чистую миграцию, и -если мы перезапишем эту таблицу, то с высокой вероятностью сломаем наш проект). - -Настало время проверить, что наше web-приложение Django работает. - -Временно откроем порт _8080_ через `iptables`: -```shell -sudo iptables -A INPUT -i eth0 -p tcp --dport 8080 -j ACCEPT -``` - -Запустим приложение с dev-режиме на нашем IP и этом порту: -```shell -python manage.py runserver :8080 -``` - -Если мы обратимся из браузера по адресу __http://:8080__, то увидим как на нашей виртуалке "бежит лог". -В браузере будет отображаться что-то похожее на наш проект (с тем отличием, что не будет работать статика... т.е. -мы не увидим картинок, не будут подгружены стили, JavaScript и все такое). - -Завершаем работу web-сервера разработчика, нажав `Control+C`. - -## 8. Конфигурируем nginx под наш проект ------------ - -Создаем и правим конфиг `/cadpoint/config/cadpoint.conf`: -```shell -nano /cadpoint/config/cadpoint.conf -``` - -Помещаем в него следующее: -```nginx configuration -# Разработка сайта CADPOINT.RU -# == Конфикурационный файл nginx cadpoint.conf - -# Описываем апстрим-потоки которые должен подключить Nginx -# Для каждого сайта надо настроить свйо поток, со своим уникальным именем. -# Если будете настраивать несколько python (django) сайтов - измените название upstream - -upstream cadpoint-django { - # расположение файла Unix-сокет для взаимодействие с uwsgi - server unix:///home//cadpoint/socket/cadpoint.sock; - # /home//cadpoint/socket/cadpoint.sock; - # также можно использовать веб-сокет (порт) для взаимодействие с uwsgi. Но это медленнее - # server 127.0.0.1:8001; # для взаимодействия с uwsgi через веб-порт - keepalive_requests 200; -} - -# конфигурируем сервер -server { - # server_name ; # доменное имя сайта - server_name cadpoint.ru; # доменное имя сайта - listen 80; - charset utf-8; # кодировка по умолчанию - access_log /home//cadpoint/logs/cadpoint-access.log; # логи с доступом - error_log /home//cadpoint/logs/cadpoint-error.log; # логи с ошибками - client_max_body_size 100M; # максимальный объем файла для загрузки на сайт (max upload size) - error_page 404 /404.html; - error_page 500 /500.html; - - location /media { alias /home//cadpoint/public/media; } # Расположение media-файлов Django - location /static { alias /home//cadpoint/public/static; } # Расположение static-файлов Django - - location /robots.txt { root /home//cadpoint/public; } # Расположение robots.txt - location /favicon.ico { root /home//cadpoint/public; } # Расположение favicon.ico - location /favicon.gif { root /home//cadpoint/public; } # Расположение favicon - location /favicon.png { root /home//cadpoint/public; } # Расположение favicon - location /favicon.svg { root /home//cadpoint/public; } # Расположение favicon - location /author.txt { root /home//cadpoint/public; } # Расположение author.txt - location = /404.html { - root /home//cadpoint/cadpoint/templates/404.html; - internal; - } - location = /500.html { - root /home//cadpoint/cadpoint/templates/500.html; - internal; - } - # location ~ \.(xml|html|htm)$ - location ~ \.(html|htm|ico|svg|png|gif|jpg|jpeg)$ { - root /home//cadpoint/public; # Расположение статичных *.xml, *.html и *.txt - } - - location / { - uwsgi_pass cadpoint-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; # вдруг некоторые запросы очень долго обрабатываются? - uwsgi_send_timeout 200; # на всякий случай время записи в сокет побольше... - } -} - -# переадресация с www на "без" www -server { - server_name www.cadpoint.ru; - listen 80; - return 301 http://cadpoint.ru$request_uri; -} -``` - -Делаем симлинк этого конфигурационного файла в папку конфигов сайтов nginx: -```shell -sudo ln -s ~/cadpoint/config/cadpoint.conf /etc/nginx/sites-enabled/ -``` - -Протестируем конфигурацию: -```shell -sudo nginx -t -``` - -Если все ок, можем "мягко" перезапустить nginx: -```shell -sudo nginx -s reload -``` - -Или "по-жёсткому": -```shell -sudo service nginx restart -``` - -Теперь если мы из браузера обратимся по адресу _http://cadpoint.ru/robots.txt_, _http://cadpoint.ru/favicon.ico_, -_http://2cadpoint.ru/favicon.png_ или _http://cadpointu/favicon.svg_ то увидим соответсвующий контент. - -## 9. НАСТРОЙКА uWSGI - -И uWSGI, и Python-плагин uWSGI уже должен быть установлен. Но если он не установлены, или для проверки, инсталляция -производится из так: -```shell -sudo apt-get install uwsgi uwsgi-plugins-all -``` - -Проверим, что uWSGI работает: -```shell -sudo service uwsgi status -``` - -Увидим примерно следующее: -``` -● uwsgi.service - LSB: Start/stop uWSGI server instance(s) - Loaded: loaded (/etc/init.d/uwsgi; generated) - Active: active (exited) since Thu 2022-01-20 20:36:20 MSK; 2min 57s ago - Docs: man:systemd-sysv-generator(8) - Tasks: 0 (limit: 1037) - Memory: 0B - CGroup: /system.slice/uwsgi.service -``` - -Создаём и правим ini-конфиг `~/cadpoint/config/cadpoint.ini` для uWSGI: -```shell -nano ~/cadpoint/config/cadpoint.conf -``` - -Помещаем туда следующее: -```editorconfig -# === Конфикурационный файл uwsgi cadpoint.ini -[uwsgi] - -# НАСТРОЙКИ ДЛЯ DJANGO -# Корневая папка проекта (полный путь) -chdir = /home//cadpoint/cadpoint -# Django wsgi файл cadpoint/wsgi.py записываем так: -module = cadpoint.wsgi -# полный путь к виртуальному окружению -home = /home//cadpoint/env -# полный путь к файлу сокета -socket = /home//cadpoint/socket/cadpoint.sock -# Исходящие сообщения в лог -daemonize = /home//cadpoint/logs/cadpoint_uwsgi.log - -# ЗАГАДОЧНЫЕ НАСТРОЙКИ, ПО ИДЕЕ ОНИ НУЖНЫ, НО И БЕЗ НИХ ВСЁ РАБОТАЕТ -# расположение wsgi.py -wsgi-file = /home//cadpoint/cadpoint/cadpoint/wsgi.py -# расположение виртуального окружения (как оно работает если этот параметр не указан, не ясно) -virtualenv = /home//cadpoint/env -# имя файла при изменении которого происходит авторестарт приложения -# (когда этого параметра нет, то гичего не авторестартится, но с ним все рестартится. -# Cтоит изменить любой Python-исходник проекта, как изменения сразу вступают в силу. -touch-reload = /home//cadpoint/logs/cadpoint_reload -py-autoreload = 5 - -# НАСТРОЙКИ ОБЩИЕ -# быть master-процессом -master = true -# максимальное количество процессов -processes = 1 -# если uWSGI устнаовлен как сервис через apt-get то нужно установить еще плугин: -# sudo apt-get install uwsgi-plugin-python -# и добавить в этот конфиг: plugin = python -plugin = python3 - # права доступа к файлу сокета. По умолчанию должно хватать 664. Но каких-то прав не хватает, поэтому 666. -chmod-socket = 666 -# очищать окружение от служебных файлов uwsgi по завершению -vacuum = true -# количество секунд после которых подвисший процес будет перезапущен -# Так как некоторе скрипты требуют изрядно времени (особенно полная переиндексация) то ставим значение побольще -harakiri = 2600 -# В общем случае, при некотых значениях harakiri логах uWSGI может вываливаться предупреждение: -# WARNING: you have enabled harakiri without post buffering. Slow upload could be rejected on post-unbuffered webservers -# можно оставить harakiri закоментированным, но нам нужно 900 и на него не ругается. Ругается на 30. - -# разрешаем многопоточность -enable-threads = true -vacuum = true -thunder-lock = true -max-requests = 500 - -# пользователь и группа пользователей от имени которых запускать uWSGI -# указываем www-data: к этой группе относится nginz, и ранее мы включили в эту группу нашего [user] -uid = root -gid = root - - -print = ---------------- Запущен uWSGI для cadpoint ---------------- -``` - -Делаем симлинк этого конфигурационного файла в папку конфигов uwsgi: -```shell -sudo ln -s ~/cadpoint/config/cadpoint.ini /etc/uwsgi/apps-enabled/ -``` - -Не забываем перезапустить uwsgi: -```shell -sudo service uwsgi restart -``` - -Еще раз проверим, что uWSGI работает: -```shell -sudo service uwsgi status -``` - -Все. Сайт развёрнут. Далее можно проводить улучшайзинг и усиливать защиту: - -## 10. Нагрузочное тестирование (нужен dockers) - -Нагрузочное тестирование будем проводить при помощи Яндекс.Танк. Он будет "обстреливать" наш сайт запросами. - -Для запускать Яндекс.Танк проще всего использовать Dockers. Т.о. Dockers должен быть установлен на машину, с которой -будем "обстреливать" сайт (и это не виртуалка хостинга... "обстреливать" самих себя бессмысленно). - -Создаем каталог `yandex.tank` (в моем случае `M:/VM/yandex.tank`) и в нем помещаем файл `load.yaml` следующего -содержания (не забываем корректировать URL, если необходимо): -```yaml -overload: - enabled: true - package: yandextank.plugins.DataUploader - token_file: "token.txt" -phantom: - address: 2022.rsvo.ru:80 - header_http: "1.1" - headers: - - "[Host: 2022.rsvo.ru]" - - "[Connection: close]" - uris: - - / - - /about/ - - /about/management - - /about/news - - /about/news/184-fgup-rsvo-prinyalo-uchastie-v-ucheniyah-mchs?p=0&n=5 - - /about/about/news/186-uchebnyij-tsentr-fgup-rsvo-prinyal-uchastie-v-mero?p=1&n=2 - - /about/news/132-gromkij-maks-2017?p=30&n=1 - - /about/news/128-fgup-rsvo-prinyalo-uchastie-v-dne-innovatsij-mchs?p=29&n=2 - - /wired-radio-broadcasting/moskva-abonentam-yuridicheskim-litsam - - /upac - - /integrated-security/solutions-for-industrial - - /integrated-security/safe-cities-solutions - - /sound-technical-support/ - - /about/contacts - - /about/smi - - /about/nagradyi-i-blagodarnosti - load_profile: - load_type: rps - # schedule: line(5, 5000, 3m) - # schedule: const(1,30s) line(1,1000,2m) const(10000,5m) - schedule: line(10,5000,2m) const(100,1m) - ssl: true -autostop: - autostop: - - http(5xx,10%,5s) -console: - enabled: true -telegraf: - enabled: false -``` - -Так же создаем пустой файл `token.txt` в той же папке. __ВНИМАНИЕ__ файл должен быть в кодировке ANSI (хоть он и пустой, -но это важно). - -Теперь можем запустить Яндекс.Танк: -```shell -docker run --rm -v M:/VM/yandex.tank:/var/loadtest -it direvius/yandex-tank -``` - -## 11. Установка SSL Let's Encrypt (переход с http на https) - -Хорошую инструкцию по установке Let's Encript: -https://www.digitalocean. -com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-22-04 - -Let’s Encrypt — это удостоверяющий центр сертификации (ЦС), который предоставляет доступный способ получения и -установки бесплатных сертификатов TLS/SSL , тем самым обеспечивая шифрование HTTPS на веб-серверах. Let’s Encrypt -предоставляет программный клиент Certbot, который пытается автоматизировать большинство (если не все) -необходимые шаги установки сертификатов, настройки web-севера nginx (и Apache тоже) и периодическое обновления -сертификатов. - -### Установка Certbot - -Если на сервере ранее была установлена более старая версия certbot, следует удалить ее: -```shell -sudo apt remove certbot -``` - -Certbot рекомендует использовать для установки пакет моментальных снимков (снапшотов). Пакеты Snap работают почти со -всеми дистрибутивами Linux, но для управления пакетами Snap требуется, чтобы вы сначала установили snapd. Ubuntu 22. -04 поставляется с поддержкой снапшотов «из коробки», поэтому нужно начать с того, что убедитесь, что ядро snapd -обновлено: -```shell -sudo snap install core; sudo snap refresh core -``` - -После этого можно установить certbot: -```shell -sudo snap install --classic certbot -``` - -Наконец, свяжем certbot с командой создания снапшотов. Это не обязательно, но по умолчанию снимки обычно менее -навязчивы, поэтому создают случайных конфликтов с какими-либо другими системными пакетами: -```shell -sudo ln -s /snap/bin/certbot /usr/bin/certbot -``` - -Теперь, Certbot установлен. Можно запустить его и получить наш сертификат. - -### Получение сертификата и тюнинг конфигурации Nginx - -Certbot должен иметь возможность найти правильный блок `server` в конфигурации Nginx. В частности, он ищет -директиву `server_name`, соответствующую домену, для которого вы запрашиваете сертификат. В нашем случае, это -домен `cadpoint.ru`. В предыдущем пункте мы создали этот конфигурационный файл в каталоге -`~/cadpoint/config/cadpoint.conf` и связали его через симлинк с каталогом настроек nginx -`/etc/nginx/sites-enabled/`. Плагин Certbot Nginx позаботится о перенастройке этого конфигурационного файла и -перезагрузке nginx - -Получаем сертификаты -```shell -sudo certbot --nginx -d cadpoint.ru -d www.cadpoint.ru -``` - -При выполнени будет предложено ввести адрес электронной почты и согласиться с условиями обслуживания. После -этого вы должны увидеть сообщение о том, что процесс прошел успешно и где хранятся ваши сертификаты: -``` -IMPORTANT NOTES: -Successfully received certificate. -Certificate is saved at: /etc/letsencrypt/live/cadpoint.ru/fullchain.pem -Key is saved at: /etc/letsencrypt/live/cadpoint.ru/privkey.pem -This certificate expires on 2022-11-01. -These files will be updated when the certificate renews. -Certbot has set up a scheduled task to automatically renew this certificate in the background. - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -If you like Certbot, please consider supporting our work by: -* Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate -* Donating to EFF: https://eff.org/donate-le -``` - -Серификат загружен, конфигурация nginx изменена и, даже, настроен редирект c http на https... В принципе на этом можно -закончить. Но будет не лишним добавить немного тюнинга. Certbot в конфиг nginx для нашего сайта подключает файл -`/etc/letsencrypt/options-ssl-nginx.conf` (он единый для nginx подключается в конфиги всех сайтов работающих по ssl -с сертификатом Let's Encrypt). Нужно внести изменения в него: -```shell -sudo nano /etc/letsencrypt/options-ssl-nginx.conf -``` - -В частности нам нужно изменить дерективы `ssl_protocols`, `ssl_ciphers`, `resolver`, `keepalive_requests` и -`ssl_session_cache`. В результате наш файл должен выглядеть так: -```nginx configuration -# This file contains important security parameters. If you modify this file -# manually, Certbot will be unable to automatically provide future security -# updates. Instead, Certbot will print and log an error message with a path to -# the up-to-date file that you will need to refer to when manually updating -# this file. Contents are based on https://ssl-config.mozilla.org - -# ssl_session_cache shared:le_nginx_SSL:10m; -ssl_session_cache builtin:1000 shared:le_nginx_SSL:25m; -ssl_session_timeout 1440m; -ssl_session_tickets off; - -# ssl_protocols SSLv2 TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; -ssl_protocols SSLv2 SSLv3 TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; -ssl_prefer_server_ciphers on; - -# ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; -ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; - -resolver 77.88.8.8; -keepalive_requests 200; -``` - -Сохраняем конфигурационый файл `Ctrl+O` и `Enter`, а выходим из редактора `Ctrl+X`. И еще надо добавить поддержку -cnfylfhnf http2 (много поточная загрузка web-страниц). Откроем конфигурационный файл nginx для нашего сайта: -```shell -nano ~/cadpoint/config/cadpoint.conf -``` - -И добавим изменим директиву: -```nginx configuration - listen 443 ssl; # managed by Certbot -``` - -на директиву: -```nginx configuration - listen 443 ssl http2; # managed by Certbot -``` - -Перезагружаем nginx чтобы применились изменения: -```shell -sudo service nginx restart -``` - -## 12. Блокируем и баним зловредов (бот-сканеры и http-досеров) - -### Ограничения числа запросов в nginx (не обязательно, но вдруг) - -Настроим nginx на ограничения числа запросов c одного IP-адреса с помощью модуля Limit Req Module. - -Открываем файл `/etc/nginx/nginx.conf` (конфиг для всех сайтов на сервере): -```shell -sudo nano /etc/nginx/nginx.conf -``` - -И в блок `http {…}` добавляем строку: -```nginx configuration -http { - ... - limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; - ... -} -``` - -где: -* $binary_remote_addr — переменная Nginx содержащая IP с которого пришёл запрос; -* zone=one — имя зоны. Если настройка делается в нескольких vhost файлах, имена должны быть разные; -* 10m — размер зоны. 1m может содержать 16000 состояний, т.е. 16000 уникальных IP-адресов; -* rate=1r/s — разрешено 1 запрос в секунду. Так же можно указать количество запросов в минуту (30r/m — 30 запросов в минуту) - -Затем открываем на редактирование конфигурационный файл nginx для нашего сайта: -```shell -nano ~/cadpoint/config/cadpoint.conf -``` - -И в блок `server {…}`, добавим строку: -```nginx configuration - limit_req zone=one burst=20 nodelay; -``` - -где: -* one — имя зоны настроеной в /etc/nginx/nginx.conf (для всех сайтов сервера) в блоке `http {…}`; -* burst — максимальный всплеск активности, можно регулировать до какого значения запросов -* в секунду может быть всплеск запросов; -* nodelay — незамедлительно, при достижении лимита подключений, выдавать код 503 -* (Service Unavailable) для этого IP. - -Строку `limit_req zone=one burst=5 nodelay;` можно добавить как непосредственно в блок `server {…}`, и тогда будет ограничиваться число запросов ко всем файлам сайта, так и в расположенные в нем блоки `location … {…}`. Целесообразнее ограничить число запросов к `uwsgi_pass`, т.к. он отвечает только за странички сайта (без статики... статика может быть запрощена клиентом довольна, т.к. одна страничка может содержать внутри себя много встроенных картинок, стилей и скриптов). - -Таким образом наш блок `location … {…}` будет выглядеть так: -```nginx configuration - location / { - uwsgi_pass cadpoint-django; # upstream обрабатывающий обращений - include uwsgi_params; # конфигурационный файл uwsgi; - proxy_set_header Host $host; - limit_req zone=one burst=5 nodelay; - fastcgi_keep_conn on; - uwsgi_read_timeout 1800; - uwsgi_send_timeout 200; - } -} -``` - -### Баним ботов и подозрительную активность - -Настроим fail2ban для выявления ботов, которые ищут скрипты, дампы баз, логи, ключи и тому подобное, анализируя error-лог. Создаем конфиг-файл фильтра `/etc/fail2ban/filter.d/nginx-noscript.conf`: -```shell -sudo nano /etc/fail2ban/filter.d/nginx-noscript.conf -``` - -Со следующим содержанием: -```editorconfig -[Definition] -failregex = .*client: .*GET.*(\.php|\.asp|\.aspx|\.exe|\.pl|\.cgi|\.scgi|\.log|\.sql|\.jsp|\.csv|\.sh|\.key|\.py|\.pyc|\.asmx|\.asax) -# failregex = ^ -.*GET.*(\.php|\.asp|\.aspx|\.exe|\.pl|\.cgi|\.scgi|\.log|\.sql|\.jsp|\.csv|\.sh|\.key|\.py|\.pyc|\.asmx|\.asax) -ignoreregex = -``` - -Проверим, что фильтр написан верно: -```shell -fail2ban-regex ~/cadpoint/logs/cadpoint-error.log /etc/fail2ban/filter.d/nginx-noscript.conf -``` - -Создаем еще одни фильтр для выявления подозрительной активности (ищут адинки популярных cms, log-и, ключи и тому подобное) анализируя access лог `/etc/fail2ban/filter.d/nginx-manual.conf`: -```shell -sudo nano /etc/fail2ban/filter.d/nginx-manual.conf -``` - -со следующим содержанием: -```editorconfig -[Definition] -failregex = ^ -.*GET.*wp-content/ - ^ -.*GET.*wp-admin/.* - ^ -.*GET.*wp-includes/.* - ^ -.*GET.*administrator/ - ^ -.*GET.*user/register/ - ^ -.*GET.*bitrix/admin/ - ^ -.*GET.*minify/ - ^ -.*GET.*(?:a|A)dmin/ - ^ -.*GET.*netcat/ - ^ -.*GET.*koobooCMS/ - ^ -.*GET.*apanel/ - ^ -.*GET.*netcat/ - ^ -.*GET*/\.git/config - ^ -.*GET*/\.well-known/ - ^ -.*GET.*(\.sql|\.php5|\.mdb|\.db|\.yml|\.cgi|\.scgi|\.log|\.sql|\.jsp|\.csv|\.sh|\.key|\.py|\.pyc|\.asmx|\.asax) -ignoreregex = -``` - -Снова проверим, что фильтр написан верно: -```shell -fail2ban-regex ~/cadpoint/logs/cadpoint-access.log /etc/fail2ban/filter.d/nginx-manual.conf -``` - -Далее, редактируем файл `/etc/fail2ban/jail.local` для настройки параметров подключения и работы фильтра: -```shell -sudo nano /etc/fail2ban/jail.local -``` - -Дописываем строки: -```editorconfig -# ФИЛЬТР nginx-noscript -[nginx-noscript] -enabled = true -filter = nginx-noscript -port = http,https -action = iptables-multiport[name=NoAuthFailures, port="http,https"] -logpath = /var/log/nginx/*error*.log - /home//cadpoint/logs/*error*.log -bantime = 86400 -maxretry = 6 -findtime = 7200 - -# ФИЛЬТР manual -[nginx-manual] -enabled = true -filter = nginx-manual -port = http,https -action = iptables-multiport[name=NoAuthFailures, port="http,https"] -logpath = /var/log/nginx/*access*.log - /home//cadpoint/logs/*access*.log -bantime = 86400 -maxretry = 6 -findtime = 7200 -``` - -Перезапустим fail2ban: -```shell -sudo service fail2ban restart -``` - -Чтобы посмотреть информацию по заблокированным ip по нашему фильтру используем команду: -```shell -sudo fail2ban-client status nginx-noscript -sudo fail2ban-client status nginx-manual -``` - -Чтобы разбанить IP (например 8.8.8.8) можно воспользоваться командой: -```shell -sudo fail2ban-client set nginx-noscript unbanip 8.8.8.8 -``` - -## 13. Ротация логов - -Обычно процесс ротации логов в системе уже установлен и работает. Но на всякий случай установить его можно так: -```shell -sudo apt-get install logrotate -``` - -Для настройки ротации логов нашего сайта нужно отредактировать конфигурационный файл ротации для nginx `/etc/logrotate.d/nginx` -```shell -sudo nano /etc/logrotate.d/nginx -``` - -И добавить в него (в конце) следующее: -```editorconfig -/home//cadpoint/logs/*.log { - daily - missingok - rotate 14 - compress - delaycompress - notifempty - create 0644 www-data adm - sharedscripts - prerotate - if [ -d /etc/logrotate.d/httpd-prerotate ]; then \ - run-parts /etc/logrotate.d/httpd-prerotate; \ - fi \ - endscript - postrotate - invoke-rc.d nginx rotate >/dev/null 2>&1 - endscript -} -``` - -После чего процесс ротации надо перезапустить: -```shell -sudo service logrotate restart -``` - -Убедимся, что ротация работает: -```shell -sudo service logrotate status -``` - -Увидим: -``` -● logrotate.service - Rotate log files - Loaded: loaded (/lib/systemd/system/logrotate.service; static; vendor preset: enabled) - Active: inactive (dead) since Wed 2022-05-18 16:47:06 MSK; 4s ago -TriggeredBy: ● logrotate.timer - Docs: man:logrotate(8) - man:logrotate.conf(5) - Process: 20860 ExecStart=/usr/sbin/logrotate /etc/logrotate.conf (code=exited, status=0/SUCCESS) - Main PID: 20860 (code=exited, status=0/SUCCESS) -``` - -## 14. Геограничения -? https://stackoverflow.com/questions/62213884/how-install-the-geoip2-module-on-a-nginx-running-in-a-production-environment - - -https://docs.nginx.com/nginx/admin-guide/installing-nginx/installing-nginx-plus/ -https://docs.nginx.com/nginx/admin-guide/dynamic-modules/geoip2/ - -https://nginx.org/ru/docs/http/ngx_http_geoip_module.html -https://dev.maxmind.com/?lang=en - From 74de66ccddbbb3fdea65c5d86e909e344d23acaf Mon Sep 17 00:00:00 2001 From: erjemin Date: Wed, 15 Apr 2026 15:38:31 +0300 Subject: [PATCH 15/16] mod: add default media storage --- cadpoint/cadpoint/settings.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/cadpoint/cadpoint/settings.py b/cadpoint/cadpoint/settings.py index c93fbd8..36886ec 100644 --- a/cadpoint/cadpoint/settings.py +++ b/cadpoint/cadpoint/settings.py @@ -156,6 +156,18 @@ CSRF_TRUSTED_ORIGINS = env.list('DJANGO_CSRF_TRUSTED_ORIGINS', default=[]) # Внутренние адреса для debug toolbar: локальный браузер и loopback. 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 в админке. # Держим их здесь, чтобы не размазывать магические числа по `admin.py`. SELECT2_AJAX_DELAY_MS = 250 @@ -232,10 +244,8 @@ NUM_ITEMS_IN_PAGE = NUM_NAV_ITEMS_IN_PAGE if DEBUG: # В деве оставляем стандартную отдачу статики Django без WhiteNoise. - STORAGES = { - 'staticfiles': { - 'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage', - }, + STORAGES['staticfiles'] = { + 'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage', } # Django Debug Toolbar нужен только в dev def _show_debug_toolbar(request): @@ -251,10 +261,8 @@ if DEBUG: else: # В проде WhiteNoise обслуживает собранную статику и файлы из `public`. MIDDLEWARE.insert(1, 'whitenoise.middleware.WhiteNoiseMiddleware') - STORAGES = { - 'staticfiles': { - 'BACKEND': 'whitenoise.storage.CompressedManifestStaticFilesStorage', - }, + STORAGES['staticfiles'] = { + 'BACKEND': 'whitenoise.storage.CompressedManifestStaticFilesStorage', } # Конфигурация WhiteNoise для обслуживания статических файлов и файлов из /public (например, # robots.txt, favicon.ico и т.п.) From d14f67d930f54ec2f2cacda98423b33fa4e51315 Mon Sep 17 00:00:00 2001 From: erjemin Date: Wed, 15 Apr 2026 16:36:34 +0300 Subject: [PATCH 16/16] minor: - --- .gitignore | 2 ++ README.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b8e8fbf..b73fb58 100644 --- a/.gitignore +++ b/.gitignore @@ -331,3 +331,5 @@ cython_debug/ *.sql my_secret*.py .github/ +# для картинок +_4image_and_design/ diff --git a/README.md b/README.md index ec5f87f..5498cf2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # CADpoint.ru -Сайт CADpoint.ru — это Django-проект, который сейчас живёт в Docker. +Сайт CADpoint.ru — это Django-проект, новостной сайт (блог о 3D-печать и Систем Автоматизированного Проектирования) на Django который сейчас живёт в Docker. Развернут по адресу [cadpoint.ru](https://cadpoint.ru). Кратко о схеме: