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` |
|
| Хост | VDS kzntsv `89.253.255.94` |
|
||||||
| Стек Portainer | **Id 28**, name `sched`, endpointId **1** |
|
| Стек 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` |
|
| 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` |
|
| 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` |
|
| БД | 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` |
|
| Admin API | `https://sched.vds.kzntsv.site` (traefik websecure, letsEncrypt). `SCHED_ADMIN_KEY` = `pass sched/admin-key` |
|
||||||
| mem_limit | `512m` (канон app-стеков) |
|
| 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).
|
Креды → `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:
|
**2. tasks.json.** Ядро стартует с `{"tasks":[]}` (воркеры — отдельными тасками). Закинуть на VDS:
|
||||||
```bash
|
```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.
|
# sched — custom single-binary (embedded engine + admin API + UI morda + MCP + MariaDB).
|
||||||
# Прод: VDS рядом с общей MariaDB (стек 11, сеть shared-dbs, отдельная база sched — свой role, решение 2026-08-27).
|
# Один контейнер, один HTTP-сервер :8080: /api/* (admin API), /mcp (Streamable HTTP MCP),
|
||||||
# Daemon: published @schedjs/daemon@0.12.1 + @schedjs/storage-mysql@0.4.0 (Dockerfile.mysql).
|
# / (UI morda), /sched-ui.bundle.js (static UI bundle).
|
||||||
# Endpoint: https://sched.vds.kzntsv.site (admin API /api/*; health /api/health открыт, остальное Bearer SCHED_ADMIN_KEY).
|
# Образ 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).
|
# Deploy: Portainer-managed (см. portainer-stack-management-vds). Env через Portainer (не env_file).
|
||||||
|
|
||||||
services:
|
services:
|
||||||
schedd:
|
sched:
|
||||||
image: registry.kzntsv.site/sched-daemon:0.12.1-mysql
|
image: registry.kzntsv.site/sched-custom:0.12.1-mysql
|
||||||
container_name: schedd
|
container_name: sched
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
mem_limit: 512m
|
mem_limit: 512m
|
||||||
networks:
|
networks:
|
||||||
- shared-dbs
|
- shared-dbs
|
||||||
- proxy
|
- proxy
|
||||||
command:
|
|
||||||
- --tasks
|
|
||||||
- /app/config/tasks.json
|
|
||||||
- --storage
|
|
||||||
- mysql
|
|
||||||
- --admin-port
|
|
||||||
- "8080"
|
|
||||||
- --admin-host
|
|
||||||
- 0.0.0.0
|
|
||||||
environment:
|
environment:
|
||||||
|
SCHED_TASKS: /app/config/tasks.json
|
||||||
MYSQL_URL: mysql://sched:${SCHED_DB_PASS}@mariadb:3306/sched?ssl={"rejectUnauthorized":false}
|
MYSQL_URL: mysql://sched:${SCHED_DB_PASS}@mariadb:3306/sched?ssl={"rejectUnauthorized":false}
|
||||||
SCHED_ADMIN_KEY: ${SCHED_ADMIN_KEY}
|
SCHED_ADMIN_KEY: ${SCHED_ADMIN_KEY}
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
Reference in New Issue
Block a user