feat(docker): start temporary local registry for buildx dependency resolution and ensure buildx builder uses host network

This commit is contained in:
2026-02-06 15:53:32 +00:00
parent d1785aab86
commit 37dfde005e
4 changed files with 160 additions and 62 deletions

View File

@@ -1,5 +1,15 @@
# Changelog # 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) ## 2026-02-06 - 1.10.0 - feat(classes.dockerfile)
support using a local base image as a build context in buildx commands support using a local base image as a build context in buildx commands

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@git.zone/tsdocker', name: '@git.zone/tsdocker',
version: '1.10.0', version: '1.11.0',
description: 'develop npm modules cross platform with docker' description: 'develop npm modules cross platform with docker'
} }

View File

@@ -10,6 +10,10 @@ const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash', 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 * Class Dockerfile represents a Dockerfile on disk
*/ */
@@ -135,6 +139,53 @@ export class Dockerfile {
return sortedDockerfileArray; 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<void> {
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<void> {
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<void> {
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 * Builds the corresponding real docker image for each Dockerfile class instance
*/ */
@@ -144,7 +195,13 @@ export class Dockerfile {
): Promise<Dockerfile[]> { ): Promise<Dockerfile[]> {
const total = sortedArrayArg.length; const total = sortedArrayArg.length;
const overallStart = Date.now(); const overallStart = Date.now();
const useRegistry = Dockerfile.needsLocalRegistry(sortedArrayArg, options);
if (useRegistry) {
await Dockerfile.startLocalRegistry();
}
try {
for (let i = 0; i < total; i++) { for (let i = 0; i < total; i++) {
const dockerfileArg = sortedArrayArg[i]; const dockerfileArg = sortedArrayArg[i];
const progress = `(${i + 1}/${total})`; const progress = `(${i + 1}/${total})`;
@@ -153,8 +210,7 @@ export class Dockerfile {
const elapsed = await dockerfileArg.build(options); const elapsed = await dockerfileArg.build(options);
logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`); logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`);
// Tag the built image with the full base image references used by dependent Dockerfiles, // Tag in host daemon for standard docker build compatibility
// so their FROM lines resolve to the locally-built image instead of pulling from a registry.
const dependentBaseImages = new Set<string>(); const dependentBaseImages = new Set<string>();
for (const other of sortedArrayArg) { for (const other of sortedArrayArg) {
if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) { if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) {
@@ -165,6 +221,16 @@ export class Dockerfile {
logger.log('info', `Tagging ${dockerfileArg.buildTag} as ${fullTag} for local dependency resolution`); logger.log('info', `Tagging ${dockerfileArg.buildTag} as ${fullTag} for local dependency resolution`);
await smartshellInstance.exec(`docker tag ${dockerfileArg.buildTag} ${fullTag}`); 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);
}
}
} finally {
if (useRegistry) {
await Dockerfile.stopLocalRegistry();
}
} }
logger.log('info', `Total build time: ${formatDuration(Date.now() - overallStart)}`); logger.log('info', `Total build time: ${formatDuration(Date.now() - overallStart)}`);
@@ -366,6 +432,7 @@ export class Dockerfile {
public baseImage: string; public baseImage: string;
public localBaseImageDependent: boolean; public localBaseImageDependent: boolean;
public localBaseDockerfile!: Dockerfile; public localBaseDockerfile!: Dockerfile;
public localRegistryTag?: string;
constructor(managerRefArg: TsDockerManager, options: IDockerfileOptions) { constructor(managerRefArg: TsDockerManager, options: IDockerfileOptions) {
this.managerRef = managerRefArg; this.managerRef = managerRefArg;
@@ -412,9 +479,12 @@ export class Dockerfile {
let buildContextFlag = ''; let buildContextFlag = '';
if (this.localBaseImageDependent && this.localBaseDockerfile) { if (this.localBaseImageDependent && this.localBaseDockerfile) {
const fromImage = this.baseImage; const fromImage = this.baseImage;
const localTag = this.localBaseDockerfile.buildTag; if (this.localBaseDockerfile.localRegistryTag) {
buildContextFlag = ` --build-context "${fromImage}=docker-image://${localTag}"`; // BuildKit pulls from the local registry (reachable via host network)
logger.log('info', `Using local build context: ${fromImage} -> docker-image://${localTag}`); 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; let buildCommand: string;

View File

@@ -148,17 +148,21 @@ export class TsDockerManager {
const total = toBuild.length; const total = toBuild.length;
const overallStart = Date.now(); const overallStart = Date.now();
const useRegistry = Dockerfile.needsLocalRegistry(toBuild, options);
if (useRegistry) {
await Dockerfile.startLocalRegistry();
}
try {
for (let i = 0; i < total; i++) { for (let i = 0; i < total; i++) {
const dockerfileArg = toBuild[i]; const dockerfileArg = toBuild[i];
const progress = `(${i + 1}/${total})`; const progress = `(${i + 1}/${total})`;
const skip = await cache.shouldSkipBuild(dockerfileArg.cleanTag, dockerfileArg.content); const skip = await cache.shouldSkipBuild(dockerfileArg.cleanTag, dockerfileArg.content);
if (skip) { if (skip) {
logger.log('ok', `${progress} Skipped ${dockerfileArg.cleanTag} (cached)`); logger.log('ok', `${progress} Skipped ${dockerfileArg.cleanTag} (cached)`);
continue; } else {
}
// Cache miss — build this Dockerfile
logger.log('info', `${progress} Building ${dockerfileArg.cleanTag}...`); logger.log('info', `${progress} Building ${dockerfileArg.cleanTag}...`);
const elapsed = await dockerfileArg.build({ const elapsed = await dockerfileArg.build({
platform: options?.platform, platform: options?.platform,
@@ -167,15 +171,11 @@ export class TsDockerManager {
verbose: options?.verbose, verbose: options?.verbose,
}); });
logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`); logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`);
const imageId = await dockerfileArg.getId(); const imageId = await dockerfileArg.getId();
cache.recordBuild(dockerfileArg.cleanTag, dockerfileArg.content, imageId, dockerfileArg.buildTag); cache.recordBuild(dockerfileArg.cleanTag, dockerfileArg.content, imageId, dockerfileArg.buildTag);
} }
logger.log('info', `Total build time: ${formatDuration(Date.now() - overallStart)}`); // Tag for dependents IMMEDIATELY (not after all builds)
// Perform dependency tagging for all Dockerfiles (even cache hits, since tags may be stale)
for (const dockerfileArg of toBuild) {
const dependentBaseImages = new Set<string>(); const dependentBaseImages = new Set<string>();
for (const other of toBuild) { for (const other of toBuild) {
if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) { if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) {
@@ -186,8 +186,19 @@ export class TsDockerManager {
logger.log('info', `Tagging ${dockerfileArg.buildTag} as ${fullTag} for local dependency resolution`); logger.log('info', `Tagging ${dockerfileArg.buildTag} as ${fullTag} for local dependency resolution`);
await smartshellInstance.exec(`docker tag ${dockerfileArg.buildTag} ${fullTag}`); 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();
}
} }
logger.log('info', `Total build time: ${formatDuration(Date.now() - overallStart)}`);
cache.save(); cache.save();
} else { } else {
// === STANDARD MODE: build all via static helper === // === STANDARD MODE: build all via static helper ===
@@ -226,20 +237,27 @@ export class TsDockerManager {
*/ */
private async ensureBuildx(): Promise<void> { private async ensureBuildx(): Promise<void> {
logger.log('info', 'Setting up Docker buildx for multi-platform builds...'); 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'); const inspectResult = await smartshellInstance.exec('docker buildx inspect tsdocker-builder 2>/dev/null');
if (inspectResult.exitCode !== 0) { if (inspectResult.exitCode !== 0) {
// Create a new buildx builder logger.log('info', 'Creating new buildx builder with host network...');
logger.log('info', 'Creating new buildx builder...'); await smartshellInstance.exec(
await smartshellInstance.exec('docker buildx create --name tsdocker-builder --use'); 'docker buildx create --name tsdocker-builder --driver docker-container --driver-opt network=host --use'
);
await smartshellInstance.exec('docker buildx inspect --bootstrap');
} else {
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'); await smartshellInstance.exec('docker buildx inspect --bootstrap');
} else { } else {
// Use existing builder
await smartshellInstance.exec('docker buildx use tsdocker-builder'); await smartshellInstance.exec('docker buildx use tsdocker-builder');
} }
}
logger.log('ok', 'Docker buildx ready'); logger.log('ok', 'Docker buildx ready');
} }