docs: мануал развёртывания в прод
Полная первичная установка на боевой сервер (LXC за Traefik, gunicorn, nginx, PostgreSQL, Redis c requirepass, Celery worker+beat), настройка интеграций (Mattermost/Nextcloud/OnlyOffice/почта/AD/PNMS-PVMS-MSorter), бэкапы, обновление и типичные грабли. Дополняет перенос-на-прод.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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 <REPO_URL> 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`) |
|
||||
```
|
||||
Reference in New Issue
Block a user