From 9479a07ddfb1f78dd38449c53abb650be8d1ff4a Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Wed, 18 Mar 2026 16:22:02 +0000 Subject: [PATCH] feat(docker): add OCI container startup configuration and migrate Docker release pipeline to tsdocker --- .gitea/workflows/docker_nottags.yaml | 2 +- .gitea/workflows/docker_tags.yaml | 14 ++-- Dockerfile | 40 +++-------- changelog.md | 7 ++ npmextra.json | 11 ++- package.json | 2 + ts/00_commitinfo_data.ts | 2 +- ts/index.ts | 23 +++++- ts_oci_container/index.ts | 100 +++++++++++++++++++++++++++ ts_oci_container/plugins.ts | 7 ++ ts_web/00_commitinfo_data.ts | 2 +- 11 files changed, 165 insertions(+), 45 deletions(-) create mode 100644 ts_oci_container/index.ts create mode 100644 ts_oci_container/plugins.ts diff --git a/.gitea/workflows/docker_nottags.yaml b/.gitea/workflows/docker_nottags.yaml index c8b6565..c24f4bc 100644 --- a/.gitea/workflows/docker_nottags.yaml +++ b/.gitea/workflows/docker_nottags.yaml @@ -6,7 +6,7 @@ on: - '**' env: - IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci + IMAGE: code.foss.global/host.today/ht-docker-node:szci NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}} NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}} diff --git a/.gitea/workflows/docker_tags.yaml b/.gitea/workflows/docker_tags.yaml index 5af83c9..d2cafa0 100644 --- a/.gitea/workflows/docker_tags.yaml +++ b/.gitea/workflows/docker_tags.yaml @@ -6,7 +6,7 @@ on: - '*' env: - IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci + IMAGE: code.foss.global/host.today/ht-docker-node:szci NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}} NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}} @@ -74,7 +74,7 @@ jobs: if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest container: - image: registry.gitlab.com/hosttoday/ht-docker-dbase:npmci + image: code.foss.global/host.today/ht-docker-node:dbase_dind steps: - uses: actions/checkout@v3 @@ -82,15 +82,13 @@ jobs: - name: Prepare run: | pnpm install -g pnpm - pnpm install -g @shipzone/npmci + pnpm install -g @git.zone/tsdocker - name: Release run: | - npmci docker login - npmci docker build - npmci docker test - # npmci docker push gitea.lossless.digital - npmci docker push dockerregistry.lossless.digital + tsdocker login + tsdocker build + tsdocker push metadata: needs: test diff --git a/Dockerfile b/Dockerfile index e85966b..ed3bc41 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,44 +1,24 @@ # gitzone dockerfile_service ## STAGE 1 // BUILD -FROM registry.gitlab.com/hosttoday/ht-docker-node:npmci as node1 +FROM code.foss.global/host.today/ht-docker-node:lts AS build COPY ./ /app WORKDIR /app -ARG NPMCI_TOKEN_NPM2 -ENV NPMCI_TOKEN_NPM2 $NPMCI_TOKEN_NPM2 -RUN npmci npm prepare RUN pnpm config set store-dir .pnpm-store RUN rm -rf node_modules && pnpm install RUN pnpm run build +RUN rm -rf .pnpm-store node_modules && pnpm install --prod + +## STAGE 2 // PRODUCTION +FROM code.foss.global/host.today/ht-docker-node:alpine-node AS production + +# gcompat + libstdc++ for glibc-linked Rust binaries (smartproxy, smartmta, remoteingress) +RUN apk add --no-cache gcompat libstdc++ -# gitzone dockerfile_service -## STAGE 2 // install production -FROM registry.gitlab.com/hosttoday/ht-docker-node:npmci as node2 WORKDIR /app -COPY --from=node1 /app /app -RUN rm -rf .pnpm-store -ARG NPMCI_TOKEN_NPM2 -ENV NPMCI_TOKEN_NPM2 $NPMCI_TOKEN_NPM2 -RUN npmci npm prepare -RUN pnpm config set store-dir .pnpm-store -RUN rm -rf node_modules/ && pnpm install --prod +COPY --from=build /app /app +ENV DCROUTER_MODE=OCI_CONTAINER -## STAGE 3 // rebuild dependencies for alpine -FROM registry.gitlab.com/hosttoday/ht-docker-node:alpinenpmci as node3 -WORKDIR /app -COPY --from=node2 /app /app -ARG NPMCI_TOKEN_NPM2 -ENV NPMCI_TOKEN_NPM2 $NPMCI_TOKEN_NPM2 -RUN npmci npm prepare -RUN pnpm config set store-dir .pnpm-store -RUN pnpm rebuild -r - -## STAGE 4 // the final production image with all dependencies in place -FROM registry.gitlab.com/hosttoday/ht-docker-node:alpine as node4 -WORKDIR /app -COPY --from=node3 /app /app - -### Healthchecks RUN pnpm install -g @servezone/healthy HEALTHCHECK --interval=30s --timeout=30s --start-period=30s --retries=3 CMD [ "healthy" ] diff --git a/changelog.md b/changelog.md index 0ff2f8f..b2543ad 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-03-18 - 11.3.0 - feat(docker) +add OCI container startup configuration and migrate Docker release pipeline to tsdocker + +- adds OCI container mode startup that reads DcRouter options from environment variables and an optional JSON config file +- simplifies the Docker image to a two-stage build with production dependencies only and Alpine runtime compatibility packages +- updates Gitea workflows and npm scripts to use tsdocker for image build and release + ## 2026-03-18 - 11.2.56 - fix(deps) bump @serve.zone/remoteingress to ^4.9.0 diff --git a/npmextra.json b/npmextra.json index ad7b1e2..90fd1b6 100644 --- a/npmextra.json +++ b/npmextra.json @@ -72,9 +72,14 @@ "dockerRegistryRepoMap": { "registry.gitlab.com": "code.foss.global/serve.zone/dcrouter" }, - "dockerBuildargEnvMap": { - "NPMCI_TOKEN_NPM2": "NPMCI_TOKEN_NPM2" - }, "npmRegistryUrl": "verdaccio.lossless.digital" + }, + "@git.zone/tsdocker": { + "registries": ["code.foss.global"], + "registryRepoMap": { + "code.foss.global": "serve.zone/dcrouter", + "dockerregistry.lossless.digital": "serve.zone/dcrouter" + }, + "platforms": ["linux/amd64", "linux/arm64"] } } \ No newline at end of file diff --git a/package.json b/package.json index f692484..39075aa 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "start": "(node --max_old_space_size=250 ./cli.js)", "startTs": "(node cli.ts.js)", "build": "(tsbuild tsfolders --allowimplicitany && npm run bundle)", + "build:docker": "tsdocker build", + "release:docker": "tsdocker push", "bundle": "(tsbundle)", "watch": "tswatch" }, diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 67c9db4..ed16819 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '11.2.56', + version: '11.3.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts/index.ts b/ts/index.ts index 6f47028..a139914 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -5,6 +5,7 @@ export { UnifiedEmailServer } from '@push.rocks/smartmta'; export type { IUnifiedEmailServerOptions, IEmailRoute, IEmailDomainConfig } from '@push.rocks/smartmta'; // DcRouter +import { DcRouter } from './classes.dcrouter.js'; export * from './classes.dcrouter.js'; // RADIUS module @@ -13,4 +14,24 @@ export * from './radius/index.js'; // Remote Ingress module export * from './remoteingress/index.js'; -export const runCli = async () => {}; +export const runCli = async () => { + let options: import('./classes.dcrouter.js').IDcRouterOptions = {}; + + if (process.env.DCROUTER_MODE === 'OCI_CONTAINER') { + const { getOciContainerConfig } = await import('../ts_oci_container/index.js'); + options = getOciContainerConfig(); + console.log('[DCRouter] Starting in OCI Container mode...'); + } + + const dcRouter = new DcRouter(options); + await dcRouter.start(); + console.log('[DCRouter] Running. Send SIGTERM or SIGINT to stop.'); + + const shutdown = async () => { + console.log('[DCRouter] Shutting down...'); + await dcRouter.stop(); + process.exit(0); + }; + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); +}; diff --git a/ts_oci_container/index.ts b/ts_oci_container/index.ts new file mode 100644 index 0000000..bfba5b4 --- /dev/null +++ b/ts_oci_container/index.ts @@ -0,0 +1,100 @@ +import * as plugins from './plugins.js'; +import type { IDcRouterOptions } from '../ts/classes.dcrouter.js'; + +/** + * Parses a comma-separated env var into a string array. + * Returns undefined if the env var is not set or empty. + */ +function parseCommaSeparated(envVar: string | undefined): string[] | undefined { + if (!envVar || envVar.trim() === '') return undefined; + return envVar.split(',').map((s) => s.trim()).filter(Boolean); +} + +/** + * Parses a comma-separated env var into a number array. + * Returns undefined if the env var is not set or empty. + */ +function parseCommaSeparatedNumbers(envVar: string | undefined): number[] | undefined { + const parts = parseCommaSeparated(envVar); + if (!parts) return undefined; + return parts.map((s) => parseInt(s, 10)).filter((n) => !isNaN(n)); +} + +/** + * Builds IDcRouterOptions from environment variables for OCI container mode. + * + * If DCROUTER_CONFIG_PATH is set and the file exists, it is loaded as a JSON base config. + * Individual env vars are then applied as overrides on top. + */ +export function getOciContainerConfig(): IDcRouterOptions { + let options: IDcRouterOptions = {}; + + // Load JSON config file if specified + const configPath = process.env.DCROUTER_CONFIG_PATH; + if (configPath && plugins.fs.existsSync(configPath)) { + const raw = plugins.fs.readFileSync(configPath, 'utf8'); + options = JSON.parse(raw); + console.log(`[OCI Container] Loaded config from ${configPath}`); + } + + // Apply env var overrides + if (process.env.DCROUTER_BASE_DIR) { + options.baseDir = process.env.DCROUTER_BASE_DIR; + } + + // TLS config + const tlsEmail = process.env.DCROUTER_TLS_EMAIL; + const tlsDomain = process.env.DCROUTER_TLS_DOMAIN; + if (tlsEmail || tlsDomain) { + options.tls = { + ...options.tls, + contactEmail: tlsEmail || options.tls?.contactEmail || '', + ...(tlsDomain ? { domain: tlsDomain } : {}), + }; + } + + // Network config + if (process.env.DCROUTER_PUBLIC_IP) { + options.publicIp = process.env.DCROUTER_PUBLIC_IP; + } + + const proxyIps = parseCommaSeparated(process.env.DCROUTER_PROXY_IPS); + if (proxyIps) { + options.proxyIps = proxyIps; + } + + // DNS config + const nsDomains = parseCommaSeparated(process.env.DCROUTER_DNS_NS_DOMAINS); + if (nsDomains) { + options.dnsNsDomains = nsDomains; + } + + const dnsScopes = parseCommaSeparated(process.env.DCROUTER_DNS_SCOPES); + if (dnsScopes) { + options.dnsScopes = dnsScopes; + } + + // Email config + const emailHostname = process.env.DCROUTER_EMAIL_HOSTNAME; + const emailPorts = parseCommaSeparatedNumbers(process.env.DCROUTER_EMAIL_PORTS); + if (emailHostname || emailPorts) { + options.emailConfig = { + ...options.emailConfig, + ...(emailHostname ? { hostname: emailHostname } : {}), + ...(emailPorts ? { ports: emailPorts } : {}), + domains: options.emailConfig?.domains || [], + routes: options.emailConfig?.routes || [], + } as IDcRouterOptions['emailConfig']; + } + + // Cache config + const cacheEnabled = process.env.DCROUTER_CACHE_ENABLED; + if (cacheEnabled !== undefined) { + options.cacheConfig = { + ...options.cacheConfig, + enabled: cacheEnabled === 'true', + }; + } + + return options; +} diff --git a/ts_oci_container/plugins.ts b/ts_oci_container/plugins.ts new file mode 100644 index 0000000..bdfc9da --- /dev/null +++ b/ts_oci_container/plugins.ts @@ -0,0 +1,7 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +export { + fs, + path, +}; diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 67c9db4..ed16819 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '11.2.56', + version: '11.3.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' }