sched: custom single-binary (embedded engine+admin api+morda+mcp+maria) — artifacts in .admin; compose on sched-custom
This commit is contained in:
@@ -25,10 +25,10 @@ updated: 2026-08-27
|
||||
|---|---|
|
||||
| Хост | VDS kzntsv `89.253.255.94` |
|
||||
| Стек Portainer | **Id 28**, name `sched`, endpointId **1** |
|
||||
| Образ | `registry.kzntsv.site/sched-daemon:0.12.1-mysql` |
|
||||
| Образ | `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` |
|
||||
| Даемон | 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-стеков) |
|
||||
@@ -62,7 +62,14 @@ docker exec mariadb mariadb -uroot -p"$ROOT_PW" -e "
|
||||
```
|
||||
Креды → `pass insert -m sched/mysql` (user/pass/db/host).
|
||||
|
||||
**1. Образ** — собрать (см. выше) → `docker login registry.kzntsv.site -u vitya` → `docker push`. (внешний registry, публичный npm, BuildKit-секрет не нужен).
|
||||
**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
|
||||
|
||||
29
host-stacks/vds-kzntsv/sched-custom/Dockerfile.custom
Normal file
29
host-stacks/vds-kzntsv/sched-custom/Dockerfile.custom
Normal file
@@ -0,0 +1,29 @@
|
||||
# sched custom single-binary — embedded engine + admin API + UI morda + MCP + storage (MariaDB).
|
||||
# Built on published @schedjs/* npm packages (artifacts that passed the gates).
|
||||
# Runs apps/daemon/src/custom-entry.mjs — one HTTP server :8080 routing /api, /mcp, / (morda).
|
||||
#
|
||||
# Build:
|
||||
# docker build -f apps/daemon/Dockerfile.custom -t registry.kzntsv.site/sched-custom:0.12.1-mysql apps/daemon
|
||||
# Run:
|
||||
# docker run -e MYSQL_URL=mysql://sched:pw@mariadb:3306/sched?ssl={"rejectUnauthorized":false} \
|
||||
# -e SCHED_ADMIN_KEY=... -e SCHED_TASKS=/app/config/tasks.json \
|
||||
# registry.kzntsv.site/sched-custom:0.12.1-mysql
|
||||
|
||||
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 @schedjs/admin-api@0.3.0 @schedjs/mcp@0.4.1 @schedjs/ui@0.3.0 mysql2@3 \
|
||||
&& rm -f .yarnrc.yml \
|
||||
&& yarn cache clean
|
||||
|
||||
ENV PATH="/app/node_modules/.bin:${PATH}"
|
||||
COPY src/custom-entry.mjs /app/custom-entry.mjs
|
||||
# Default tasks.json baked into the image (overridden on prod by a bind mount / SCHED_TASKS).
|
||||
RUN echo '{"tasks":[]}' > /app/tasks.json
|
||||
EXPOSE 8080
|
||||
VOLUME ["/data"]
|
||||
ENTRYPOINT ["node", "/app/custom-entry.mjs"]
|
||||
93
host-stacks/vds-kzntsv/sched-custom/custom-entry.mjs
Normal file
93
host-stacks/vds-kzntsv/sched-custom/custom-entry.mjs
Normal file
@@ -0,0 +1,93 @@
|
||||
// sched custom single-binary: embedded engine + admin API + UI morda + MCP (Streamable HTTP) + storage (MariaDB/MySQL).
|
||||
// One HTTP server on :8080, routed:
|
||||
// /api/* → createAdminApi.handleRequest (health open, rest Bearer SCHED_ADMIN_KEY)
|
||||
// /mcp → createMcpHttpHandler (Streamable HTTP MCP, projection of admin API)
|
||||
// /sched-ui.bundle.js → static UI bundle
|
||||
// / → UI morda (html)
|
||||
// Engine via createDaemon (runs ticks + sync; we supply storage+adminApi ourselves on OUR server).
|
||||
import { createServer } from 'node:http';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { createDaemon } from '@schedjs/daemon';
|
||||
import { createAdminApi } from '@schedjs/admin-api';
|
||||
import { createMysqlStorage, dbNameFromUrl } from '@schedjs/storage-mysql';
|
||||
import { createPool } from 'mysql2/promise';
|
||||
import { AdminApiClient } from '@schedjs/mcp';
|
||||
|
||||
// Subpath imports are blocked by npm `exports` masks — import the dist modules by
|
||||
// absolute file URL (a direct file reference, not a package specifier).
|
||||
const { createMcpHttpHandler } = await import(pathToFileURL('/app/node_modules/@schedjs/mcp/dist/http.js').href);
|
||||
const { mordaHtml, DEFAULT_UI_BUNDLE } = await import(pathToFileURL('/app/node_modules/@schedjs/ui/dist/morda.js').href);
|
||||
|
||||
const PORT = Number(process.env.SCHED_PORT ?? 8080);
|
||||
const HOST = process.env.SCHED_HOST ?? '0.0.0.0';
|
||||
const ADMIN_KEY = process.env.SCHED_ADMIN_KEY; // Bearer; unset → open (dev)
|
||||
const TASKS = process.env.SCHED_TASKS ?? '/app/tasks.json';
|
||||
|
||||
// --- storage (MariaDB/MySQL) ---
|
||||
const MYSQL_URL = process.env.MYSQL_URL;
|
||||
if (!MYSQL_URL) throw new Error('MYSQL_URL is not set — export MYSQL_URL=<connstring>');
|
||||
const pool = createPool(MYSQL_URL);
|
||||
const storage = await createMysqlStorage(pool); // self-migrates on open
|
||||
const dbName = dbNameFromUrl(MYSQL_URL);
|
||||
|
||||
// --- daemon: engine only (we own the http server) ---
|
||||
const daemon = createDaemon({
|
||||
storage,
|
||||
tasksPath: TASKS,
|
||||
// no `admin` here — createDaemon would bind its own /api-only server; we route ourselves
|
||||
});
|
||||
await daemon.start();
|
||||
const engine = daemon.engine;
|
||||
const adminApi = createAdminApi({ engine, storage, auth: ADMIN_KEY ? { apiKey: ADMIN_KEY } : undefined, version: '0.12.1-custom' });
|
||||
|
||||
// --- MCP: projection of admin API via HTTP client to ourselves ---
|
||||
// AdminApiClient appends paths like '/tasks' to baseUrl → must end at the /api mount point.
|
||||
const ADMIN_BASE = `http://127.0.0.1:${PORT}/api`;
|
||||
const mcpClient = new AdminApiClient(ADMIN_BASE, ADMIN_KEY);
|
||||
const mcpHandler = createMcpHttpHandler({ client: mcpClient, readonly: process.env.SCHED_MCP_READONLY === '1' });
|
||||
|
||||
// --- static UI bundle bytes ---
|
||||
let uiBundle = null;
|
||||
try { uiBundle = readFileSync(DEFAULT_UI_BUNDLE); } catch { /* bundle missing → morda 404s its script */ }
|
||||
|
||||
const server = createServer(async (req, res) => {
|
||||
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
||||
const p = url.pathname;
|
||||
try {
|
||||
if (p === '/api' || p.startsWith('/api/')) {
|
||||
req.url = p.replace(/^\/api/, '') + url.search; // strip /api mount prefix
|
||||
await adminApi.handleRequest(req, res);
|
||||
return;
|
||||
}
|
||||
if (p === '/mcp' || p.startsWith('/mcp/')) {
|
||||
await mcpHandler(req, res);
|
||||
return;
|
||||
}
|
||||
if (p === '/sched-ui.bundle.js' && uiBundle) {
|
||||
res.writeHead(200, { 'content-type': 'application/javascript' });
|
||||
res.end(uiBundle);
|
||||
return;
|
||||
}
|
||||
// UI morda for everything else (root + non-api paths)
|
||||
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
||||
res.end(mordaHtml({ apiBase: '/api', bundleSrc: '/sched-ui.bundle.js', tokenKey: 'sched-token' }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'content-type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(PORT, HOST, () => {
|
||||
process.stderr.write(`sched custom up: http://${HOST}:${PORT} (storage=mysql db=${dbName}, admin_auth=${ADMIN_KEY ? 'on' : 'OFF'}, mcp=on)\n`);
|
||||
});
|
||||
|
||||
// Graceful stop on SIGTERM
|
||||
for (const sig of ['SIGTERM', 'SIGINT']) {
|
||||
process.on(sig, () => {
|
||||
daemon.stop();
|
||||
server.close();
|
||||
pool.end();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
@@ -1,28 +1,23 @@
|
||||
# sched — шедулер (ядро daemon + admin API) на инфра-VDS.
|
||||
# Прод: VDS рядом с общей MariaDB (стек 11, сеть shared-dbs, отдельная база sched — свой role, решение 2026-08-27).
|
||||
# Daemon: published @schedjs/daemon@0.12.1 + @schedjs/storage-mysql@0.4.0 (Dockerfile.mysql).
|
||||
# Endpoint: https://sched.vds.kzntsv.site (admin API /api/*; health /api/health открыт, остальное Bearer SCHED_ADMIN_KEY).
|
||||
# sched — custom single-binary (embedded engine + admin API + UI morda + MCP + MariaDB).
|
||||
# Один контейнер, один HTTP-сервер :8080: /api/* (admin API), /mcp (Streamable HTTP MCP),
|
||||
# / (UI morda), /sched-ui.bundle.js (static UI bundle).
|
||||
# Образ registry.kzntsv.site/sched-custom:0.12.1-mysql = published @schedjs/{daemon,storage-mysql,admin-api,mcp,ui}
|
||||
# + собственный custom-entry.mjs (Dockerfile.custom).
|
||||
# Прод: VDS рядом с общей MariaDB (стек 11, сеть shared-dbs, отдельная база sched — свой role).
|
||||
# Endpoint: https://sched.vds.kzntsv.site — /api/* + /mcp, остальное → UI morda.
|
||||
# Deploy: Portainer-managed (см. portainer-stack-management-vds). Env через Portainer (не env_file).
|
||||
|
||||
services:
|
||||
schedd:
|
||||
image: registry.kzntsv.site/sched-daemon:0.12.1-mysql
|
||||
container_name: schedd
|
||||
sched:
|
||||
image: registry.kzntsv.site/sched-custom:0.12.1-mysql
|
||||
container_name: sched
|
||||
restart: unless-stopped
|
||||
mem_limit: 512m
|
||||
networks:
|
||||
- shared-dbs
|
||||
- proxy
|
||||
command:
|
||||
- --tasks
|
||||
- /app/config/tasks.json
|
||||
- --storage
|
||||
- mysql
|
||||
- --admin-port
|
||||
- "8080"
|
||||
- --admin-host
|
||||
- 0.0.0.0
|
||||
environment:
|
||||
SCHED_TASKS: /app/config/tasks.json
|
||||
MYSQL_URL: mysql://sched:${SCHED_DB_PASS}@mariadb:3306/sched?ssl={"rejectUnauthorized":false}
|
||||
SCHED_ADMIN_KEY: ${SCHED_ADMIN_KEY}
|
||||
volumes:
|
||||
|
||||
Reference in New Issue
Block a user