diff --git a/docs/развёртывание-прод.md b/docs/развёртывание-прод.md new file mode 100644 index 0000000..462a55a --- /dev/null +++ b/docs/развёртывание-прод.md @@ -0,0 +1,361 @@ +# Развёртывание ACORE в production + +Полный мануал первичной установки портала на боевой сервер «с нуля». +Перенос данных из тестовой БД описан отдельно — см. [перенос-на-прод.md](перенос-на-прод.md). + +**Целевая топология (как в `.env.example`):** + +``` + HTTPS HTTP :8000 +клиент ───────▶ Traefik ───────▶ LXC с ACORE ──▶ PostgreSQL (10.10.31.2:5432) + (TLS) (Host rule) ├─ nginx ─▶ gunicorn (WSGI) └─ Redis (:6379, requirepass) + ├─ celery worker + └─ celery beat +``` + +- **Traefik** терминирует TLS и проксирует на LXC по HTTP с заголовком `X-Forwarded-Proto: https` + (`production.py` уже настроен на `SECURE_PROXY_SSL_HEADER`). +- **nginx внутри LXC** отдаёт `/static/` и `/media/`, остальное проксирует в gunicorn. +- **PostgreSQL** и **Redis** — отдельные сервисы (могут быть на других хостах). + +--- + +## 0. Требования + +| Компонент | Версия | Назначение | +|---|---|---| +| Python | 3.14 | как в `.venv` проекта | +| PostgreSQL | 14+ | основная БД | +| Redis | 6+ | кэш (троттлинг логина) + брокер Celery, **обязателен `requirepass`** | +| nginx | любой | статика/медиа + reverse-proxy | +| gettext | — | для `compilemessages` (uk/en) | +| gcc, libpq-dev, libldap2-dev, libsasl2-dev | — | сборка psycopg2 / ldap3 | + +Внешние интеграции (опциональны, настраиваются позже в админке): Mattermost, Nextcloud (WebDAV) + OnlyOffice Document Server, почта (IMAP/SMTP), Active Directory, порталы PNMS/PVMS/MSorter. + +--- + +## 1. Системный пользователь и код + +```bash +# от root +adduser --system --group --home /opt/acore acore +apt-get update && apt-get install -y \ + python3.14 python3.14-venv python3-pip \ + build-essential libpq-dev libldap2-dev libsasl2-dev gettext \ + nginx redis-server postgresql-client git + +sudo -u acore -H bash +cd /opt/acore +git clone app # или скопировать релиз +cd app +python3.14 -m venv .venv +.venv/bin/pip install --upgrade pip +.venv/bin/pip install -r requirements.txt +``` + +> `main.css` (Tailwind) уже собран и лежит в репозитории — npm-сборка в проде **не нужна**. +> Пересобирать только при изменении стилей (см. `ACORE-template/`). + +--- + +## 2. PostgreSQL + +На сервере БД (10.10.31.2): + +```sql +CREATE USER acore WITH PASSWORD 'СИЛЬНЫЙ_ПАРОЛЬ'; +CREATE DATABASE acore OWNER acore ENCODING 'UTF8' LC_COLLATE 'C' TEMPLATE template0; +GRANT ALL PRIVILEGES ON DATABASE acore TO acore; +``` + +`pg_hba.conf` — разрешить доступ с IP LXC по `scram-sha-256`. Проверка с LXC: +```bash +psql "postgres://acore:ПАРОЛЬ@10.10.31.2:5432/acore" -c '\conninfo' +``` + +--- + +## 3. Redis (с паролем — production это требует) + +`/etc/redis/redis.conf`: +``` +requirepass СИЛЬНЫЙ_REDIS_ПАРОЛЬ +bind 127.0.0.1 # или адрес, доступный только LXC +maxmemory-policy allkeys-lru +``` +```bash +systemctl enable --now redis-server +redis-cli -a СИЛЬНЫЙ_REDIS_ПАРОЛЬ ping # → PONG +``` + +> Продовые настройки **откажутся стартовать**, если в `CACHE_URL` / `CELERY_BROKER_URL` +> нет пароля (см. `_assert_redis_auth`). Временный обход на время cutover: +> `REDIS_ALLOW_UNAUTHENTICATED=1` (убрать сразу после установки `requirepass`). + +Используются две БД Redis: **`/0`** — брокер Celery, **`/1`** — кэш Django. + +--- + +## 4. Секреты и `.env` + +Сгенерировать два независимых секрета: +```bash +python3 -c "import secrets; print(secrets.token_urlsafe(64))" # для SECRET_KEY +python3 -c "import secrets; print(secrets.token_urlsafe(64))" # для FIELD_ENCRYPTION_KEY +``` + +> ⚠️ **`FIELD_ENCRYPTION_KEY`** — это не Fernet-ключ, а любая длинная строка +> (внутри `core/utils/crypto.py` она хэшируется в ключ). **Задать один раз и не менять** — +> смена ключа делает все зашифрованные секреты коннекторов (пароли/токены) нечитаемыми. +> Хранить в менеджере секретов. + +`/opt/acore/app/.env`: +```ini +DJANGO_SETTINGS_MODULE=acore.settings.production +DEBUG=False + +SECRET_KEY=<секрет-1> +FIELD_ENCRYPTION_KEY=<секрет-2> + +ALLOWED_HOSTS=portal.example.local,10.10.31.3 +PORTAL_BASE_URL=https://portal.example.local +CSRF_TRUSTED_ORIGINS=https://portal.example.local + +DATABASE_URL=postgres://acore:ПАРОЛЬ@10.10.31.2:5432/acore + +# Redis с паролем (две отдельные БД) +CACHE_URL=redis://:REDIS_ПАРОЛЬ@127.0.0.1:6379/1 +CELERY_BROKER_URL=redis://:REDIS_ПАРОЛЬ@127.0.0.1:6379/0 +CELERY_RESULT_BACKEND=redis://:REDIS_ПАРОЛЬ@127.0.0.1:6379/0 + +# Интеграции (заполняются позже; пустые = fail-closed) +# MATTERMOST_SLASH_COMMAND_TOKEN= +# MATTERMOST_INTERACTIVE_TOKEN= +# PNMS_SYNC_ENABLED=1 / PNMS_BASE_URL=https://... / PNMS_ACORE_TOKEN=... +# MSORTER_* / PVMS_* — см. .env.example +``` +```bash +chmod 600 /opt/acore/app/.env && chown acore:acore /opt/acore/app/.env +``` + +--- + +## 5. Миграции, статика, локали, суперпользователь + +```bash +cd /opt/acore/app +export DJANGO_SETTINGS_MODULE=acore.settings.production + +.venv/bin/python manage.py check --deploy # должно быть 0 issues +.venv/bin/python manage.py migrate +.venv/bin/python manage.py collectstatic --noinput # → staticfiles/ +.venv/bin/python manage.py compilemessages # uk/en (нужен gettext) +.venv/bin/python manage.py rebuild_directory_tree # пересборка дерева справочников +.venv/bin/python manage.py createsuperuser +``` + +> При первом входе суперпользователь **обязан включить 2FA** (middleware принуждает +> всех staff). Держите под рукой доступ к TOTP-приложению. + +--- + +## 6. gunicorn (systemd) + +`/etc/systemd/system/acore-web.service`: +```ini +[Unit] +Description=ACORE gunicorn +After=network.target + +[Service] +User=acore +Group=acore +WorkingDirectory=/opt/acore/app +EnvironmentFile=/opt/acore/app/.env +ExecStart=/opt/acore/app/.venv/bin/gunicorn acore.wsgi:application \ + --bind 127.0.0.1:8000 \ + --workers 5 \ + --timeout 120 \ + --access-logfile - --error-logfile - +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target +``` +> Воркеры: `(2 × vCPU) + 1`. Загрузки/скачивания документов и OnlyOffice-callback +> могут быть крупными — `--timeout 120`. + +--- + +## 7. Celery worker + beat (systemd) + +`/etc/systemd/system/acore-celery.service`: +```ini +[Unit] +Description=ACORE Celery worker +After=network.target redis-server.service + +[Service] +User=acore +Group=acore +WorkingDirectory=/opt/acore/app +EnvironmentFile=/opt/acore/app/.env +ExecStart=/opt/acore/app/.venv/bin/celery -A acore worker -l info --concurrency=4 +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +`/etc/systemd/system/acore-celery-beat.service`: +```ini +[Unit] +Description=ACORE Celery beat +After=network.target redis-server.service + +[Service] +User=acore +Group=acore +WorkingDirectory=/opt/acore/app +EnvironmentFile=/opt/acore/app/.env +ExecStart=/opt/acore/app/.venv/bin/celery -A acore beat -l info \ + --scheduler django_celery_beat.schedulers:DatabaseScheduler +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +```bash +systemctl daemon-reload +systemctl enable --now acore-web acore-celery acore-celery-beat +systemctl status acore-web +``` + +> Celery нужен для фоновой синхронизации (Mattermost, AD, push в PNMS/PVMS/MSorter). +> Периодические задачи задаются в админке (django-celery-beat, DatabaseScheduler). + +--- + +## 8. nginx (статика/медиа + proxy) + +`/etc/nginx/sites-available/acore`: +```nginx +server { + listen 80; + server_name portal.example.local 10.10.31.3; + + client_max_body_size 110m; # аплоады до 100 МБ (документы) + + location /static/ { alias /opt/acore/app/staticfiles/; access_log off; expires 30d; } + location /media/ { alias /opt/acore/app/media/; access_log off; } + + location / { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; # приходит от Traefik + proxy_read_timeout 120s; + } +} +``` +```bash +ln -s /etc/nginx/sites-available/acore /etc/nginx/sites-enabled/ +nginx -t && systemctl reload nginx +``` + +**Traefik** (внешний): Host-rule на `portal.example.local` → сервис `http://10.10.31.3:80`, +TLS-termination + редирект на HTTPS. Убедитесь, что Traefik проставляет +`X-Forwarded-Proto: https` (по умолчанию так и есть) — иначе `SECURE_SSL_REDIRECT` +уйдёт в цикл редиректов. + +--- + +## 9. Настройка интеграций (после запуска, в админке) + +Все секреты коннекторов шифруются в БД (`EncryptedCharField`) — вводятся в +**Админка → Настройки → Конфиги коннекторов**. + +- **Mattermost** — bot-токен + `MATTERMOST_SLASH_COMMAND_TOKEN`/`INTERACTIVE_TOKEN` в `.env`. + Команды: `create_mattermost_bot`, `import_mattermost_teams/channels`, `sync_users_to_teams`. +- **Nextcloud** — `ConnectorConfig(nextcloud)`: host/логин/пароль сервисного аккаунта (WebDAV). +- **OnlyOffice** (`ConnectorConfig(onlyoffice)`): + - `host/port/use_ssl` → публичный URL Document Server (браузер грузит `api.js`); + - **«Токен»** = `JWT_SECRET` Document Server (`JWT_ENABLED=true`, `JWT_HEADER=Authorization`); + - `extra_config`: `{"public_url": "...", "callback_base": "https://portal.example.local", "verify_ssl": true}`. + - **Сеть в обе стороны:** браузер → DS, DS → портал (`/documents/onlyoffice/content|callback`). + Если DS ходит к порталу по внутреннему адресу — добавьте его в `extra_config.download_hosts` + (иначе SSRF-guard отклонит сохранение). +- **Почта** — `ConnectorConfig(mail)`: IMAP/SMTP хост, сервисный аккаунт. +- **Active Directory** — `ConnectorConfig(ad)`; синк: `sync_users_to_ad`. +- **PNMS/PVMS/MSorter** — включаются флагами в `.env` (`*_SYNC_ENABLED=1`, `*_BASE_URL` **https**, + `*_ACORE_TOKEN`). Первичная выгрузка: `push_to_pnms` / `push_to_pvms` / `push_to_msorter`. + +--- + +## 10. Проверочный чек-лист + +```bash +export DJANGO_SETTINGS_MODULE=acore.settings.production +cd /opt/acore/app +.venv/bin/python manage.py check --deploy # 0 issues +.venv/bin/python manage.py makemigrations --check --dry-run # No changes detected +``` +- [ ] `https://portal.example.local/` открывается, редирект c http на https работает; +- [ ] логин, принудительная 2FA у staff; +- [ ] `/static/` и `/media/` отдаёт nginx (не 404); +- [ ] `systemctl status acore-web acore-celery acore-celery-beat` — все `active (running)`; +- [ ] `redis-cli -a … -n 1 dbsize` растёт при попытках логина (кэш-троттлинг жив); +- [ ] загрузка документа в аптеке и открытие в OnlyOffice → правка сохраняется в Nextcloud. + +--- + +## 11. Резервное копирование + +```bash +# БД (ежедневно, cron от пользователя postgres или acore) +pg_dump "postgres://acore:ПАРОЛЬ@10.10.31.2:5432/acore" | gzip > /backup/acore-$(date +%F).sql.gz + +# Медиа +rsync -a /opt/acore/app/media/ /backup/media/ + +# КРИТИЧНО: .env (SECRET_KEY + FIELD_ENCRYPTION_KEY) — в менеджер секретов. +# Потеря FIELD_ENCRYPTION_KEY = потеря всех секретов коннекторов. +``` + +--- + +## 12. Обновление (деплой новой версии) + +```bash +sudo -u acore -H bash +cd /opt/acore/app +git pull # или распаковать релиз +.venv/bin/pip install -r requirements.txt +export DJANGO_SETTINGS_MODULE=acore.settings.production +.venv/bin/python manage.py migrate +.venv/bin/python manage.py collectstatic --noinput +.venv/bin/python manage.py compilemessages +exit +systemctl restart acore-web acore-celery acore-celery-beat +``` + +--- + +## 13. Частые грабли + +| Симптом | Причина / решение | +|---|---| +| `ImproperlyConfigured: FIELD_ENCRYPTION_KEY must be set` | не задан ключ в `.env` | +| `ImproperlyConfigured: CACHE_URL must include a Redis password` | нет пароля в Redis-URL; задать `requirepass` или временно `REDIS_ALLOW_UNAUTHENTICATED=1` | +| Бесконечный редирект на https | Traefik не шлёт `X-Forwarded-Proto: https` / nginx не пробрасывает заголовок | +| Секреты коннекторов «поехали» после смены ключа | `FIELD_ENCRYPTION_KEY` менять нельзя после ввода секретов | +| Стили/иконки админки не грузятся | не выполнен `collectstatic` или nginx `location /static/` указывает не туда | +| OnlyOffice не сохраняет файл | не совпадает `JWT_SECRET`↔«Токен», или DS не достаёт `callback_base`; проверьте `download_hosts` | +| uk/en не переведены | не выполнен `compilemessages` (нет пакета `gettext`) | +```