diff --git a/changelog.md b/changelog.md index 40f011a..16102b3 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # Changelog +## 2026-02-06 - 1.11.0 - feat(docker) +start temporary local registry for buildx dependency resolution and ensure buildx builder uses host network + +- Introduce a temporary local registry (localhost:5234) with start/stop helpers and push support to expose local images for buildx +- Add Dockerfile.needsLocalRegistry to decide when a local registry is required (local base dependencies + multi-platform or platform option) +- Push built images to the local registry and set localRegistryTag on Dockerfile instances for BuildKit build-context usage +- Tag built images in the host daemon for dependent Dockerfiles to resolve local FROM references +- Integrate registry lifecycle into Dockerfile.buildDockerfiles and TsDockerManager build flows (start before builds, stop after) +- Ensure buildx builder is created with --driver-opt network=host and recreate existing builder if it lacks host network to allow registry access from build containers + ## 2026-02-06 - 1.10.0 - feat(classes.dockerfile) support using a local base image as a build context in buildx commands diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index e0c0437..58962fb 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@git.zone/tsdocker', - version: '1.10.0', + version: '1.11.0', description: 'develop npm modules cross platform with docker' } diff --git a/ts/classes.dockerfile.ts b/ts/classes.dockerfile.ts index 8e66de1..976077a 100644 --- a/ts/classes.dockerfile.ts +++ b/ts/classes.dockerfile.ts @@ -10,6 +10,10 @@ const smartshellInstance = new plugins.smartshell.Smartshell({ executor: 'bash', }); +const LOCAL_REGISTRY_PORT = 5234; +const LOCAL_REGISTRY_HOST = `localhost:${LOCAL_REGISTRY_PORT}`; +const LOCAL_REGISTRY_CONTAINER = 'tsdocker-local-registry'; + /** * Class Dockerfile represents a Dockerfile on disk */ @@ -135,6 +139,53 @@ export class Dockerfile { return sortedDockerfileArray; } + /** Determines if a local registry is needed for buildx dependency resolution. */ + public static needsLocalRegistry( + dockerfiles: Dockerfile[], + options?: { platform?: string }, + ): boolean { + const hasLocalDeps = dockerfiles.some(df => df.localBaseImageDependent); + if (!hasLocalDeps) return false; + const config = dockerfiles[0]?.managerRef?.config; + return !!options?.platform || !!(config?.platforms && config.platforms.length > 1); + } + + /** Starts a temporary registry:2 container on port 5234. */ + public static async startLocalRegistry(): Promise { + await smartshellInstance.execSilent( + `docker rm -f ${LOCAL_REGISTRY_CONTAINER} 2>/dev/null || true` + ); + const result = await smartshellInstance.execSilent( + `docker run -d --name ${LOCAL_REGISTRY_CONTAINER} -p ${LOCAL_REGISTRY_PORT}:5000 registry:2` + ); + if (result.exitCode !== 0) { + throw new Error(`Failed to start local registry: ${result.stderr || result.stdout}`); + } + // registry:2 starts near-instantly; brief wait for readiness + await new Promise(resolve => setTimeout(resolve, 1000)); + logger.log('info', `Started local registry at ${LOCAL_REGISTRY_HOST}`); + } + + /** Stops and removes the temporary local registry container. */ + public static async stopLocalRegistry(): Promise { + await smartshellInstance.execSilent( + `docker rm -f ${LOCAL_REGISTRY_CONTAINER} 2>/dev/null || true` + ); + logger.log('info', 'Stopped local registry'); + } + + /** Pushes a built image to the local registry for buildx consumption. */ + public static async pushToLocalRegistry(dockerfile: Dockerfile): Promise { + const registryTag = `${LOCAL_REGISTRY_HOST}/${dockerfile.buildTag}`; + await smartshellInstance.execSilent(`docker tag ${dockerfile.buildTag} ${registryTag}`); + const result = await smartshellInstance.execSilent(`docker push ${registryTag}`); + if (result.exitCode !== 0) { + throw new Error(`Failed to push to local registry: ${result.stderr || result.stdout}`); + } + dockerfile.localRegistryTag = registryTag; + logger.log('info', `Pushed ${dockerfile.buildTag} to local registry as ${registryTag}`); + } + /** * Builds the corresponding real docker image for each Dockerfile class instance */ @@ -144,26 +195,41 @@ export class Dockerfile { ): Promise { const total = sortedArrayArg.length; const overallStart = Date.now(); + const useRegistry = Dockerfile.needsLocalRegistry(sortedArrayArg, options); - for (let i = 0; i < total; i++) { - const dockerfileArg = sortedArrayArg[i]; - const progress = `(${i + 1}/${total})`; - logger.log('info', `${progress} Building ${dockerfileArg.cleanTag}...`); + if (useRegistry) { + await Dockerfile.startLocalRegistry(); + } - const elapsed = await dockerfileArg.build(options); - logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`); + try { + for (let i = 0; i < total; i++) { + const dockerfileArg = sortedArrayArg[i]; + const progress = `(${i + 1}/${total})`; + logger.log('info', `${progress} Building ${dockerfileArg.cleanTag}...`); - // Tag the built image with the full base image references used by dependent Dockerfiles, - // so their FROM lines resolve to the locally-built image instead of pulling from a registry. - const dependentBaseImages = new Set(); - for (const other of sortedArrayArg) { - if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) { - dependentBaseImages.add(other.baseImage); + const elapsed = await dockerfileArg.build(options); + logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`); + + // Tag in host daemon for standard docker build compatibility + const dependentBaseImages = new Set(); + for (const other of sortedArrayArg) { + if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) { + dependentBaseImages.add(other.baseImage); + } + } + for (const fullTag of dependentBaseImages) { + logger.log('info', `Tagging ${dockerfileArg.buildTag} as ${fullTag} for local dependency resolution`); + await smartshellInstance.exec(`docker tag ${dockerfileArg.buildTag} ${fullTag}`); + } + + // Push to local registry for buildx dependency resolution + if (useRegistry && sortedArrayArg.some(other => other.localBaseDockerfile === dockerfileArg)) { + await Dockerfile.pushToLocalRegistry(dockerfileArg); } } - for (const fullTag of dependentBaseImages) { - logger.log('info', `Tagging ${dockerfileArg.buildTag} as ${fullTag} for local dependency resolution`); - await smartshellInstance.exec(`docker tag ${dockerfileArg.buildTag} ${fullTag}`); + } finally { + if (useRegistry) { + await Dockerfile.stopLocalRegistry(); } } @@ -366,6 +432,7 @@ export class Dockerfile { public baseImage: string; public localBaseImageDependent: boolean; public localBaseDockerfile!: Dockerfile; + public localRegistryTag?: string; constructor(managerRefArg: TsDockerManager, options: IDockerfileOptions) { this.managerRef = managerRefArg; @@ -412,9 +479,12 @@ export class Dockerfile { let buildContextFlag = ''; if (this.localBaseImageDependent && this.localBaseDockerfile) { const fromImage = this.baseImage; - const localTag = this.localBaseDockerfile.buildTag; - buildContextFlag = ` --build-context "${fromImage}=docker-image://${localTag}"`; - logger.log('info', `Using local build context: ${fromImage} -> docker-image://${localTag}`); + if (this.localBaseDockerfile.localRegistryTag) { + // BuildKit pulls from the local registry (reachable via host network) + const registryTag = this.localBaseDockerfile.localRegistryTag; + buildContextFlag = ` --build-context "${fromImage}=docker-image://${registryTag}"`; + logger.log('info', `Using local registry build context: ${fromImage} -> docker-image://${registryTag}`); + } } let buildCommand: string; diff --git a/ts/classes.tsdockermanager.ts b/ts/classes.tsdockermanager.ts index db21538..35a0c71 100644 --- a/ts/classes.tsdockermanager.ts +++ b/ts/classes.tsdockermanager.ts @@ -148,46 +148,57 @@ export class TsDockerManager { const total = toBuild.length; const overallStart = Date.now(); + const useRegistry = Dockerfile.needsLocalRegistry(toBuild, options); - for (let i = 0; i < total; i++) { - const dockerfileArg = toBuild[i]; - const progress = `(${i + 1}/${total})`; - const skip = await cache.shouldSkipBuild(dockerfileArg.cleanTag, dockerfileArg.content); - if (skip) { - logger.log('ok', `${progress} Skipped ${dockerfileArg.cleanTag} (cached)`); - continue; + if (useRegistry) { + await Dockerfile.startLocalRegistry(); + } + + try { + for (let i = 0; i < total; i++) { + const dockerfileArg = toBuild[i]; + const progress = `(${i + 1}/${total})`; + const skip = await cache.shouldSkipBuild(dockerfileArg.cleanTag, dockerfileArg.content); + + if (skip) { + logger.log('ok', `${progress} Skipped ${dockerfileArg.cleanTag} (cached)`); + } else { + logger.log('info', `${progress} Building ${dockerfileArg.cleanTag}...`); + const elapsed = await dockerfileArg.build({ + platform: options?.platform, + timeout: options?.timeout, + noCache: options?.noCache, + verbose: options?.verbose, + }); + logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`); + const imageId = await dockerfileArg.getId(); + cache.recordBuild(dockerfileArg.cleanTag, dockerfileArg.content, imageId, dockerfileArg.buildTag); + } + + // Tag for dependents IMMEDIATELY (not after all builds) + const dependentBaseImages = new Set(); + for (const other of toBuild) { + if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) { + dependentBaseImages.add(other.baseImage); + } + } + for (const fullTag of dependentBaseImages) { + logger.log('info', `Tagging ${dockerfileArg.buildTag} as ${fullTag} for local dependency resolution`); + await smartshellInstance.exec(`docker tag ${dockerfileArg.buildTag} ${fullTag}`); + } + + // Push to local registry for buildx (even for cache hits — image exists but registry doesn't) + if (useRegistry && toBuild.some(other => other.localBaseDockerfile === dockerfileArg)) { + await Dockerfile.pushToLocalRegistry(dockerfileArg); + } + } + } finally { + if (useRegistry) { + await Dockerfile.stopLocalRegistry(); } - - // Cache miss — build this Dockerfile - logger.log('info', `${progress} Building ${dockerfileArg.cleanTag}...`); - const elapsed = await dockerfileArg.build({ - platform: options?.platform, - timeout: options?.timeout, - noCache: options?.noCache, - verbose: options?.verbose, - }); - logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`); - - const imageId = await dockerfileArg.getId(); - cache.recordBuild(dockerfileArg.cleanTag, dockerfileArg.content, imageId, dockerfileArg.buildTag); } logger.log('info', `Total build time: ${formatDuration(Date.now() - overallStart)}`); - - // Perform dependency tagging for all Dockerfiles (even cache hits, since tags may be stale) - for (const dockerfileArg of toBuild) { - const dependentBaseImages = new Set(); - for (const other of toBuild) { - if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) { - dependentBaseImages.add(other.baseImage); - } - } - for (const fullTag of dependentBaseImages) { - logger.log('info', `Tagging ${dockerfileArg.buildTag} as ${fullTag} for local dependency resolution`); - await smartshellInstance.exec(`docker tag ${dockerfileArg.buildTag} ${fullTag}`); - } - } - cache.save(); } else { // === STANDARD MODE: build all via static helper === @@ -226,20 +237,27 @@ export class TsDockerManager { */ private async ensureBuildx(): Promise { logger.log('info', 'Setting up Docker buildx for multi-platform builds...'); - - // Check if a buildx builder exists const inspectResult = await smartshellInstance.exec('docker buildx inspect tsdocker-builder 2>/dev/null'); if (inspectResult.exitCode !== 0) { - // Create a new buildx builder - logger.log('info', 'Creating new buildx builder...'); - await smartshellInstance.exec('docker buildx create --name tsdocker-builder --use'); + logger.log('info', 'Creating new buildx builder with host network...'); + await smartshellInstance.exec( + 'docker buildx create --name tsdocker-builder --driver docker-container --driver-opt network=host --use' + ); await smartshellInstance.exec('docker buildx inspect --bootstrap'); } else { - // Use existing builder - await smartshellInstance.exec('docker buildx use tsdocker-builder'); + const inspectOutput = inspectResult.stdout || ''; + if (!inspectOutput.includes('network=host')) { + logger.log('info', 'Recreating buildx builder with host network (migration)...'); + await smartshellInstance.exec('docker buildx rm tsdocker-builder 2>/dev/null'); + await smartshellInstance.exec( + 'docker buildx create --name tsdocker-builder --driver docker-container --driver-opt network=host --use' + ); + await smartshellInstance.exec('docker buildx inspect --bootstrap'); + } else { + await smartshellInstance.exec('docker buildx use tsdocker-builder'); + } } - logger.log('ok', 'Docker buildx ready'); }