--- title: "sched — VDS deploy runbook (ядро daemon + admin API, MariaDB, стек sched)" type: concept tags: [sched, vds, deploy, docker, portainer, traefik, mariadb, runbook, scheduler] related: [concepts/runbooks-index.md, concepts/portainer-stack-management-vds.md, concepts/sched-publish-runbook.md] sources: [] updated: 2026-08-27 --- # sched → VDS deploy runbook (ядро daemon) > **⚙️ СЛУЖЕБНЫЙ РАНБУК — только для администратора проекта `.admin`.** > Применять/выполнять шаги может только **`.admin`** (оператор проекта admin). > Из проекта не выносить: не копировать в другие вики, не пересказывать, не публиковать. > Нужен деплой или прод-операция по этому ранбуку — **поставить задачу админу (`.admin`) и написать письмо**. Развёртывание **ядра sched** (шедулер: daemon + in-process admin API) на инфра-VDS (`89.253.255.94`), storage-адаптер **MariaDB** (общая MariaDB-стек 11, отдельная база `sched`). Первый деплой — 2026-08-27 (task:790). Вводные с 2026-08-27: org/scope **@schedjs** (github.com/schedjs/schedjs), self-host на MariaDB (не sqlite). ## Параметры | Что | Значение | |---|---| | Хост | VDS kzntsv `89.253.255.94` | | Стек Portainer | **Id 28**, name `sched`, endpointId **1** | | Образ | `registry.kzntsv.site/sched-custom:0.12.1-mysql` (custom single-binary) | Compose source-of-truth | `admin/host-stacks/vds-kzntsv/sched.compose.yml` | | tasks.json source-of-truth | `admin/host-stacks/vds-kzntsv/sched.tasks.json` → VDS `/opt/stacks/sched/tasks.json` | | Даемон | published `@schedjs/daemon@0.12.1` + `@schedjs/storage-mysql@0.4.0` + `@schedjs/admin-api@0.3.0` + `@schedjs/mcp@0.4.1` + `@schedjs/ui@0.3.0` | | БД | MariaDB `mariadb:3306` (сеть `shared-dbs`), база `sched`, роль `sched@%`. Креды: `pass sched/mysql` | | Admin API | `https://sched.vds.kzntsv.site` (traefik websecure, letsEncrypt). `SCHED_ADMIN_KEY` = `pass sched/admin-key` | | mem_limit | `512m` (канон app-стеков) | | Сети | `shared-dbs` + `proxy` (обе external) | ## Артефакты - **Код/пакеты:** публикуются на npm `@schedjs/*` (см. `sched-publish-runbook`). Dev-исходники — репо `sched` (Gitea). - **Dockerfile.** Стандартный `apps/daemon/Dockerfile` собирает из `@schedjs/daemon`. Для MariaDB — **композиция** адаптера. В репо добавлен `apps/daemon/Dockerfile.mysql` (public npm, без токена): ```dockerfile FROM node:24-alpine WORKDIR /app; ENV NODE_ENV=production RUN echo '{"name":"schedd","private":true,"packageManager":"yarn@4.18.0"}' > package.json \ && corepack enable && printf 'nodeLinker: node-modules\nnpmMinimalAgeGate: 0\n' > .yarnrc.yml \ && CI=1 yarn add @schedjs/daemon@0.12.1 @schedjs/storage-mysql@0.4.0 \ && rm -f .yarnrc.yml && yarn cache clean ENV PATH="/app/node_modules/.bin:${PATH}"; EXPOSE 8080; VOLUME ["/data"]; ENTRYPOINT ["schedd"] ``` Сборка: `docker build -f apps/daemon/Dockerfile.mysql -t registry.kzntsv.site/sched-daemon:0.12.1-mysql apps/daemon`. Единственный слой, который важно композить — `@schedjs/storage-mysql` (иначе `--storage mysql` падает `ERR_MODULE_NOT_FOUND`). ## Первичный деплой (сделано 2026-08-27) **0. База (однократно).** ```bash # на VDS, root внутри mariadb контейнера docker exec mariadb mariadb -uroot -p"$ROOT_PW" -e " CREATE DATABASE IF NOT EXISTS sched CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER IF NOT EXISTS 'sched'@'%' IDENTIFIED BY ''; GRANT ALL PRIVILEGES ON sched.* TO 'sched'@'%'; FLUSH PRIVILEGES;" ``` Креды → `pass insert -m sched/mysql` (user/pass/db/host). **1. Образ (custom single-binary).** `apps/daemon/Dockerfile.custom` собирает один образ с опубликованными `@schedjs/{daemon,storage-mysql,admin-api,mcp,ui}` + `custom-entry.mjs` — один HTTP-сервер на `:8080`: - `/api/*` → админ API (health open, остальное Bearer `SCHED_ADMIN_KEY`) - `/mcp` → Streamable HTTP MCP (projection of admin API, `SCHED_MCP_READONLY=1` read-only) - `/` → UI morda (web-components) - `/sched-ui.bundle.js` → статический UI-бандл Сборка: `docker build -f apps/daemon/Dockerfile.custom -t registry.kzntsv.site/sched-custom:0.12.1-mysql apps/daemon`. → ЕДИНЫЙ бинарь: даемон + UI-морда + MCP не плодит отдельные контейнеры (обратная связь vitya). `createDaemon` даёт `engine`+`storage` (admin server НЕ bind — маршрутизация своя). MCP/морда импортируются по **file-URL** (subpath-импорты заблокированы npm `exports`-маской). **2. tasks.json.** Ядро стартует с `{"tasks":[]}` (воркеры — отдельными тасками). Закинуть на VDS: ```bash scp sched.tasks.json vds.kzntsv.site:/tmp/ ; ssh vds.kzntsv.site 'sudo mkdir -p /opt/stacks/sched && sudo cp /tmp/sched.tasks.json /opt/stacks/sched/tasks.json && sudo chmod 644 /opt/stacks/sched/tasks.json' ``` **3. Стек Portainer.** compose в `host-stacks/vds-kzntsv/sched.compose.yml`, env через Portainer API (`SCHED_DB_PASS`, `SCHED_ADMIN_KEY`). Create `standalone/string?endpointId=1`. **Ключевой момент — MYSQL_URL + TLS.** MariaDB-стек 11 запущен с `--require-secure-transport=ON` и self-signed TLS. Подключение через `mariadb:3306` (внутренняя сеть `shared-dbs`) **без TLS НЕ проходит**: `fatal: Connections using insecure transport are prohibited`. Фикс — ssl в connstring: ``` MYSQL_URL=mysql://sched:${SCHED_DB_PASS}@mariadb:3306/sched?ssl={"rejectUnauthorized":false} ``` `mysql2` парсит `ssl` из query-string (`ConnectionConfig.parseUrl`), self-signed → `rejectUnauthorized:false`. ## Реденплой (обновление образа) ```bash # 1. build + push нового тега (см. артефакты) # 2. на VDS docker pull ДО restart (иначе застрянет на старом digest) ssh vds.kzntsv.site 'docker pull registry.kzntsv.site/sched-daemon:' # 3. source-of-truth compose: сменить image: тег # 4. Portainer PUT /api/stacks/28?endpointId=1 {stackFileContent, env:[...], prune:false, pullImage:true} ``` ⚠️ PUT сбрасывает env-массив, если не передать `env` (gotcha 4b portainer-канон). ⚠️ Синхронизировать source-of-truth compose после PUT (иначе следующий redeploy из гита откатит тег). ## Verify ```bash # контейнер docker ps -a --filter name=schedd --format "{{.Status}}" # Up ... (healthy) docker logs --tail 5 schedd # "daemon started (tasks=..., storage=mysql db=sched, ...)" # health (публичный, открыт) curl -ksS https://sched.vds.kzntsv.site/api/health # {"ok":true,"uptimeMs":...,"version":"0.12.1"} # авторизованный доступ (SCHED_ADMIN_KEY = Bearer) curl -ksS -H "Authorization: Bearer $AK" https://sched.vds.kzntsv.site/api/tasks # {"tasks":[]} curl -ksS https://sched.vds.kzntsv.site/api/tasks # 401 (без Bearer) # таблицы в MariaDB docker exec mariadb mariadb -uroot -p"$ROOT_PW" -e "USE sched; SHOW TABLES; SELECT version FROM sched_schema_version;" # → scheduled_tasks, schedules, task_runs, sched_schema_version; version = 6 ``` ## Rollback PUT стека 28 с предыдущим тегом образа (или `{"tasks":[]}` пустой cron). Старые теги в registry. ## Gotchas 1. **TLS к MariaDB обязателен** — `--require-secure-transport=ON`, без `?ssl={"rejectUnauthorized":false}` → daemon в restart-loop с `fatal: insecure transport prohibited`. 2. **basicAuth — оставлен ВЫКЛ** (первый деплой). У daemon самодостаточный `SCHED_ADMIN_KEY` (Bearer) — второй слой. `/api/health` открыт (для мониторинга), всё остальное 401 без Bearer. Если нужен front-layer basicAuth — добавить middleware в traefik labels (сгенерировать bcrypt-хэш `htpasswd -nbB`), НО хэш передавать аккуратно (traefik может не принять `$2y$` при кривой передаче). 3. **`--admin-host 0.0.0.0`** — daemon слушает на всех интерфейсах внутри `proxy`-сети для traefik. Наружу VDS-порт НЕ публикуется (нет `ports:`), только через traefik. 4. **Воркеры НЕ в этом ядре** — `task:1190` (yt-digest), `task:1311` (tg-digest) отдельные; добавляются как задачи в `tasks.json`. 5. **`SCHED_ADMIN_KEY`** — в `pass sched/admin-key` (не basic-auth; это разные вещи). 6. **storage-mysql сам применяет миграции** на старте (`sched_schema_version`, версия 6) — внешний инструментарий не нужен, но БД должна быть доступна с правами на CREATE/ALTER. ## Связи - [[concepts/portainer-stack-management-vds.md]] — общий Portainer-канон (JWT, gotchas, mem_limit) - [[concepts/sched-publish-runbook.md]] — паблиш @schedjs на npm/GitHub - [[concepts/runbooks-index.md]] — индекс ранбуков - Репо sched: `docs/content/docs/13.self-hosting.md` — композиция storage-адаптеров, security model