deploy(docker): self-init entrypoint (миграции+засев прав) + гайд по TrueNAS
- docker-entrypoint.sh: при старте node migrations-runner (идемпотентно) + seed-permissions только если role_permissions пуста → контейнер поднимается на чистом томе без ручных шагов (сервер раньше fail-fast без миграций). Dockerfile: ENTRYPOINT через tini + entrypoint, нормализация CRLF (sed) + chmod, label BQ-System → LearnSpace. - DEPLOY-TRUENAS.md: пошагово для TrueNAS SCALE (датасет → образ → Custom App compose с host-path томами и JWT_SECRET → авто-миграции → reverse-proxy/HTTPS/TURN → бэкапы), заметка про CORE. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
|||||||
|
# Запуск LearnSpace на TrueNAS
|
||||||
|
|
||||||
|
Коротко: **TrueNAS SCALE** (Linux) запускает приложение как **Docker-контейнер** — в репозитории уже
|
||||||
|
есть `Dockerfile` и `docker-compose.yml`. **TrueNAS CORE** (FreeBSD) Docker не умеет — там нужен Linux-VM
|
||||||
|
или jail с Node (см. конец). Ниже — путь для **SCALE** (рекомендуемый).
|
||||||
|
|
||||||
|
Контейнер **самоинициализируется**: при старте применяет миграции БД и при первом запуске засевает права
|
||||||
|
(`docker-entrypoint.sh`). Данные (SQLite-БД, загрузки, бэкапы) хранятся в томах, переживают пересоздание.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Подготовка хранилища (dataset)
|
||||||
|
|
||||||
|
В TrueNAS создайте датасет под приложение и в нём подпапки для постоянных данных:
|
||||||
|
|
||||||
|
```
|
||||||
|
Datasets → Add Dataset: tank/apps/learnspace
|
||||||
|
```
|
||||||
|
Внутри (через Shell или по SMB) создайте три папки — БД, загрузки, бэкапы:
|
||||||
|
```
|
||||||
|
/mnt/tank/apps/learnspace/data
|
||||||
|
/mnt/tank/apps/learnspace/uploads
|
||||||
|
/mnt/tank/apps/learnspace/backups
|
||||||
|
```
|
||||||
|
Они монтируются в контейнер; их защищают снапшоты/репликация TrueNAS.
|
||||||
|
|
||||||
|
## 2. Доставить образ на NAS (3 варианта)
|
||||||
|
|
||||||
|
**A. Собрать на машине с Docker и перенести** (без реестра):
|
||||||
|
```bash
|
||||||
|
# на ПК с Docker, в корне репозитория:
|
||||||
|
docker build -t learnspace:latest .
|
||||||
|
docker save learnspace:latest | gzip > learnspace.tar.gz
|
||||||
|
# скопировать learnspace.tar.gz на NAS (SMB/scp), затем в Shell TrueNAS:
|
||||||
|
docker load < /mnt/tank/apps/learnspace/learnspace.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
**B. Через реестр** (Docker Hub / GHCR / локальный): `docker push <registry>/learnspace:latest`,
|
||||||
|
а в compose указать этот `image:`.
|
||||||
|
|
||||||
|
**C. Собрать прямо на NAS** (SCALE ElectricEel 24.10+ имеет Docker CLI): склонировать репозиторий в
|
||||||
|
датасет и `docker compose build` (см. ниже).
|
||||||
|
|
||||||
|
## 3. Развернуть как Custom App (Compose YAML)
|
||||||
|
|
||||||
|
TrueNAS SCALE → **Apps → Discover Apps → Custom App → Install via YAML** (на ElectricEel+ это нативный
|
||||||
|
docker-compose). Вставьте (правьте пути пула, порт и **обязательно JWT_SECRET**):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: learnspace:latest # из шага 2 (или build: . если собираете на NAS)
|
||||||
|
container_name: learnspace
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3000:3000" # снаружи:внутри. Поменяйте левую часть при конфликте
|
||||||
|
volumes:
|
||||||
|
- /mnt/tank/apps/learnspace/data:/app/backend/data
|
||||||
|
- /mnt/tank/apps/learnspace/uploads:/app/backend/uploads
|
||||||
|
- /mnt/tank/apps/learnspace/backups:/app/backups
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=3000
|
||||||
|
- JWT_SECRET=ЗАМЕНИТЕ_на_длинную_случайную_строку # обязательно! напр. `openssl rand -hex 32`
|
||||||
|
- JWT_EXPIRES_IN=7d
|
||||||
|
- DB_PATH=/app/backend/data/learnspace.db
|
||||||
|
- UPLOADS_DIR=/app/backend/uploads
|
||||||
|
- CLIENT_ORIGIN=https://learn.example.com # публичный адрес (для CORS); или http://<ip>:3000
|
||||||
|
# необязательно — TURN для видеосвязи за NAT (классы/доска):
|
||||||
|
# - TURN_URL=turn:turn.example.com:3478
|
||||||
|
# - TURN_USER=...
|
||||||
|
# - TURN_PASS=...
|
||||||
|
# необязательно — LLM «Спроси» (Groq и т.п.):
|
||||||
|
# - ASSISTANT_LLM_URL=https://api.groq.com/openai/v1/chat/completions
|
||||||
|
# - ASSISTANT_LLM_KEY=...
|
||||||
|
# - ASSISTANT_LLM_MODEL=llama-3.3-70b-versatile
|
||||||
|
```
|
||||||
|
|
||||||
|
Сгенерировать секрет: в Shell TrueNAS `openssl rand -hex 32`.
|
||||||
|
|
||||||
|
## 4. Первый запуск
|
||||||
|
|
||||||
|
При старте entrypoint сам накатит миграции и засеет права — отдельных команд не нужно. Проверьте:
|
||||||
|
- **Apps → learnspace → Logs**: должно быть `applying migrations...` → `starting server...` → `Server running on port 3000`.
|
||||||
|
- Healthcheck (`/api/health`) станет «healthy» через ~10–30 с.
|
||||||
|
|
||||||
|
Откройте `http://<IP_NAS>:3000`. Создайте первого пользователя через регистрацию (или заведите админа —
|
||||||
|
скажите, добавлю команду/скрипт сидинга админа).
|
||||||
|
|
||||||
|
## 5. HTTPS и домен (рекомендуется)
|
||||||
|
|
||||||
|
Порт 3000 — внутренний. Для доступа по домену с HTTPS поставьте reverse-proxy:
|
||||||
|
- TrueNAS app **Traefik**/**Nginx Proxy Manager**, либо внешний nginx → проксировать на `<IP_NAS>:3000`.
|
||||||
|
- В `CLIENT_ORIGIN` укажите итоговый публичный URL (иначе CORS заблокирует фронт).
|
||||||
|
- Для классов/доски (WebRTC) за CGNAT/симметричным NAT нужен TURN-сервер (coturn) — иначе p2p может не подняться.
|
||||||
|
|
||||||
|
## 6. Обновление версии
|
||||||
|
|
||||||
|
1. Пересоберите образ новой версии (шаг 2) с тем же тегом или новым.
|
||||||
|
2. Apps → learnspace → **Edit** (обновите `image:` если тег сменился) → **Update**, либо пересоздайте app.
|
||||||
|
3. Данные сохранятся (тома на датасете). Миграции применятся автоматически при старте.
|
||||||
|
|
||||||
|
## 7. Бэкапы
|
||||||
|
|
||||||
|
Данные в `/mnt/tank/apps/learnspace/{data,uploads}`. Настройте **снапшоты датасета** + при желании
|
||||||
|
**репликацию**. SQLite-файл — `data/learnspace.db` (консистентный бэкап лучше делать при остановленном app
|
||||||
|
или через снапшот ZFS).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TrueNAS CORE (FreeBSD) — без Docker
|
||||||
|
|
||||||
|
CORE не запускает Docker-контейнеры. Варианты:
|
||||||
|
- **Linux-VM** в TrueNAS CORE (если включён bhyve) → внутри VM поставить Docker и следовать инструкции SCALE выше.
|
||||||
|
- **Jail** с Node 22+: установить node, `npm ci --omit=dev` в `backend/`, прописать env, `npm run migrate &&
|
||||||
|
npm run seed:permissions`, запускать `node src/server.js` под supervisor (pm2/rc.d). node:sqlite требует Node ≥ 22.
|
||||||
|
|
||||||
|
> Какой у вас TrueNAS (SCALE или CORE) и версия — подскажу точные шаги под конкретный вариант.
|
||||||
+8
-5
@@ -6,7 +6,7 @@ RUN npm ci --omit=dev
|
|||||||
|
|
||||||
# ── Stage 2: runtime ─────────────────────────────────────────────────────
|
# ── Stage 2: runtime ─────────────────────────────────────────────────────
|
||||||
FROM node:22-alpine
|
FROM node:22-alpine
|
||||||
LABEL maintainer="BQ-System"
|
LABEL maintainer="LearnSpace"
|
||||||
|
|
||||||
RUN apk add --no-cache sqlite tini
|
RUN apk add --no-cache sqlite tini
|
||||||
|
|
||||||
@@ -17,9 +17,12 @@ COPY backend/src ./backend/src
|
|||||||
COPY backend/seed.js ./backend/seed.js
|
COPY backend/seed.js ./backend/seed.js
|
||||||
COPY frontend ./frontend
|
COPY frontend ./frontend
|
||||||
COPY js ./js
|
COPY js ./js
|
||||||
|
COPY docker-entrypoint.sh ./docker-entrypoint.sh
|
||||||
|
|
||||||
# Ensure data & uploads dirs exist (volumes mount here)
|
# Ensure data & uploads dirs exist (volumes mount here); normalize entrypoint EOL + make executable
|
||||||
RUN mkdir -p /app/backend/data /app/backend/uploads /app/backups
|
RUN mkdir -p /app/backend/data /app/backend/uploads /app/backups \
|
||||||
|
&& sed -i 's/\r$//' /app/docker-entrypoint.sh \
|
||||||
|
&& chmod +x /app/docker-entrypoint.sh
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV PORT=3000
|
ENV PORT=3000
|
||||||
@@ -28,5 +31,5 @@ EXPOSE 3000
|
|||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
CMD wget -qO- http://localhost:3000/api/health || exit 1
|
CMD wget -qO- http://localhost:3000/api/health || exit 1
|
||||||
|
|
||||||
ENTRYPOINT ["tini", "--"]
|
# Entrypoint применяет миграции + засев прав, затем запускает сервер (tini — PID 1, сигналы/зомби)
|
||||||
CMD ["node", "backend/src/server.js"]
|
ENTRYPOINT ["tini", "--", "/app/docker-entrypoint.sh"]
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# LearnSpace — инициализация контейнера: применить миграции (идемпотентно),
|
||||||
|
# при первом старте засеять права, затем запустить сервер.
|
||||||
|
set -e
|
||||||
|
cd /app/backend
|
||||||
|
|
||||||
|
echo "[entrypoint] applying migrations..."
|
||||||
|
node src/db/migrations-runner.js
|
||||||
|
|
||||||
|
# Засев прав — только если реестр пуст (первый запуск): schema-check сервера
|
||||||
|
# требует непустую role_permissions. Идемпотентно и безопасно при перезапусках.
|
||||||
|
NEED_SEED=$(node -e "try{const {DatabaseSync}=require('node:sqlite');const db=new DatabaseSync(process.env.DB_PATH||'./data/learnspace.db');const n=db.prepare('SELECT COUNT(*) AS n FROM role_permissions').get().n;process.stdout.write(n>0?'0':'1')}catch(e){process.stdout.write('1')}")
|
||||||
|
if [ "$NEED_SEED" = "1" ]; then
|
||||||
|
echo "[entrypoint] seeding permissions (first run)..."
|
||||||
|
node src/db/seed-permissions.js || echo "[entrypoint] seed-permissions skipped/failed (non-fatal)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[entrypoint] starting server..."
|
||||||
|
exec node src/server.js
|
||||||
Reference in New Issue
Block a user