Compare commits

...

4 Commits

Author SHA1 Message Date
b04b8c9033 v1.13.0
Some checks failed
Default (tags) / security (push) Successful in 40s
Default (tags) / test (push) Failing after 4m1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-07 04:33:07 +00:00
2130a8a879 feat(docker): add Docker context detection, rootless support, and context-aware buildx registry handling 2026-02-07 04:33:07 +00:00
17de78aed3 v1.12.0
Some checks failed
Default (tags) / security (push) Successful in 40s
Default (tags) / test (push) Failing after 4m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-06 16:35:49 +00:00
eddb8cd156 feat(docker): add detailed logging for buildx, build commands, local registry, and local dependency info 2026-02-06 16:35:49 +00:00
8 changed files with 157 additions and 22 deletions

View File

@@ -1,5 +1,23 @@
# Changelog
## 2026-02-07 - 1.13.0 - feat(docker)
add Docker context detection, rootless support, and context-aware buildx registry handling
- Introduce DockerContext class to detect current Docker context and rootless mode and to log warnings and context info
- Add IDockerContextInfo interface and a new context option on build/config to pass explicit Docker context
- Propagate --context CLI flag into TsDockerManager.prepare so CLI commands can set an explicit Docker context
- Make buildx builder name context-aware (tsdocker-builder-<sanitized-context>) and log builder name/platforms
- Pass isRootless into local registry startup and build pipeline; emit rootless-specific warnings and registry reachability hint
## 2026-02-06 - 1.12.0 - feat(docker)
add detailed logging for buildx, build commands, local registry, and local dependency info
- Log startup of local registry including a note about buildx dependency bridging
- Log constructed build commands and indicate whether buildx or standard docker build is used (including platforms and --push/--load distinctions)
- Emit build mode summary at start of build phase and report local base-image dependency mappings
- Report when --no-cache is enabled and surface buildx setup readiness with configured platforms
- Non-functional change: purely adds informational logging to improve observability during builds
## 2026-02-06 - 1.11.0 - feat(docker)
start temporary local registry for buildx dependency resolution and ensure buildx builder uses host network

View File

@@ -1,6 +1,6 @@
{
"name": "@git.zone/tsdocker",
"version": "1.11.0",
"version": "1.13.0",
"private": false,
"description": "develop npm modules cross platform with docker",
"main": "dist_ts/index.js",

View File

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

View File

@@ -0,0 +1,69 @@
import * as plugins from './tsdocker.plugins.js';
import { logger } from './tsdocker.logging.js';
import type { IDockerContextInfo } from './interfaces/index.js';
const smartshellInstance = new plugins.smartshell.Smartshell({ executor: 'bash' });
export class DockerContext {
public contextInfo: IDockerContextInfo | null = null;
/** Sets DOCKER_CONTEXT env var for explicit context selection. */
public setContext(contextName: string): void {
process.env.DOCKER_CONTEXT = contextName;
logger.log('info', `Docker context explicitly set to: ${contextName}`);
}
/** Detects current Docker context via `docker context inspect` and rootless via `docker info`. */
public async detect(): Promise<IDockerContextInfo> {
let name = 'default';
let endpoint = 'unknown';
const contextResult = await smartshellInstance.execSilent(
`docker context inspect --format '{{json .}}'`
);
if (contextResult.exitCode === 0 && contextResult.stdout) {
try {
const parsed = JSON.parse(contextResult.stdout.trim());
const data = Array.isArray(parsed) ? parsed[0] : parsed;
name = data.Name || 'default';
endpoint = data.Endpoints?.docker?.Host || 'unknown';
} catch { /* fallback to defaults */ }
}
let isRootless = false;
const infoResult = await smartshellInstance.execSilent(
`docker info --format '{{json .SecurityOptions}}'`
);
if (infoResult.exitCode === 0 && infoResult.stdout) {
isRootless = infoResult.stdout.includes('name=rootless');
}
this.contextInfo = { name, endpoint, isRootless, dockerHost: process.env.DOCKER_HOST };
return this.contextInfo;
}
/** Logs context info prominently. */
public logContextInfo(): void {
if (!this.contextInfo) return;
const { name, endpoint, isRootless, dockerHost } = this.contextInfo;
logger.log('info', '=== DOCKER CONTEXT ===');
logger.log('info', `Context: ${name}`);
logger.log('info', `Endpoint: ${endpoint}`);
if (dockerHost) logger.log('info', `DOCKER_HOST: ${dockerHost}`);
logger.log('info', `Rootless: ${isRootless ? 'yes' : 'no'}`);
}
/** Emits rootless-specific warnings. */
public logRootlessWarnings(): void {
if (!this.contextInfo?.isRootless) return;
logger.log('warn', '[rootless] network=host in buildx is namespaced by rootlesskit');
logger.log('warn', '[rootless] Local registry may have localhost vs 127.0.0.1 resolution quirks');
}
/** Returns context-aware builder name: tsdocker-builder-<context> */
public getBuilderName(): string {
const contextName = this.contextInfo?.name || 'default';
const sanitized = contextName.replace(/[^a-zA-Z0-9_-]/g, '-');
return `tsdocker-builder-${sanitized}`;
}
}

View File

@@ -151,7 +151,7 @@ export class Dockerfile {
}
/** Starts a temporary registry:2 container on port 5234. */
public static async startLocalRegistry(): Promise<void> {
public static async startLocalRegistry(isRootless?: boolean): Promise<void> {
await smartshellInstance.execSilent(
`docker rm -f ${LOCAL_REGISTRY_CONTAINER} 2>/dev/null || true`
);
@@ -163,7 +163,10 @@ export class Dockerfile {
}
// 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}`);
logger.log('info', `Started local registry at ${LOCAL_REGISTRY_HOST} (buildx dependency bridge)`);
if (isRootless) {
logger.log('warn', `[rootless] Registry on port ${LOCAL_REGISTRY_PORT} — if buildx cannot reach localhost:${LOCAL_REGISTRY_PORT}, try 127.0.0.1:${LOCAL_REGISTRY_PORT}`);
}
}
/** Stops and removes the temporary local registry container. */
@@ -191,14 +194,14 @@ export class Dockerfile {
*/
public static async buildDockerfiles(
sortedArrayArg: Dockerfile[],
options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean },
options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean; isRootless?: boolean },
): Promise<Dockerfile[]> {
const total = sortedArrayArg.length;
const overallStart = Date.now();
const useRegistry = Dockerfile.needsLocalRegistry(sortedArrayArg, options);
if (useRegistry) {
await Dockerfile.startLocalRegistry();
await Dockerfile.startLocalRegistry(options?.isRootless);
}
try {
@@ -492,6 +495,7 @@ export class Dockerfile {
if (platformOverride) {
// Single platform override via buildx
buildCommand = `docker buildx build --platform ${platformOverride}${noCacheFlag}${buildContextFlag} --load -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
logger.log('info', `Build: buildx --platform ${platformOverride} --load`);
} else if (config.platforms && config.platforms.length > 1) {
// Multi-platform build using buildx
const platformString = config.platforms.join(',');
@@ -499,13 +503,16 @@ export class Dockerfile {
if (config.push) {
buildCommand += ' --push';
logger.log('info', `Build: buildx --platform ${platformString} --push`);
} else {
buildCommand += ' --load';
logger.log('info', `Build: buildx --platform ${platformString} --load`);
}
} else {
// Standard build
const versionLabel = this.managerRef.projectInfo?.npm?.version || 'unknown';
buildCommand = `docker build --label="version=${versionLabel}"${noCacheFlag} -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
logger.log('info', 'Build: docker build (standard)');
}
if (timeout) {

View File

@@ -5,6 +5,7 @@ import { Dockerfile } from './classes.dockerfile.js';
import { DockerRegistry } from './classes.dockerregistry.js';
import { RegistryStorage } from './classes.registrystorage.js';
import { TsDockerCache } from './classes.tsdockercache.js';
import { DockerContext } from './classes.dockercontext.js';
import type { ITsDockerConfig, IBuildCommandOptions } from './interfaces/index.js';
const smartshellInstance = new plugins.smartshell.Smartshell({
@@ -18,17 +19,27 @@ export class TsDockerManager {
public registryStorage: RegistryStorage;
public config: ITsDockerConfig;
public projectInfo: any;
public dockerContext: DockerContext;
private dockerfiles: Dockerfile[] = [];
constructor(config: ITsDockerConfig) {
this.config = config;
this.registryStorage = new RegistryStorage();
this.dockerContext = new DockerContext();
}
/**
* Prepares the manager by loading project info and registries
*/
public async prepare(): Promise<void> {
public async prepare(contextArg?: string): Promise<void> {
// Detect Docker context
if (contextArg) {
this.dockerContext.setContext(contextArg);
}
await this.dockerContext.detect();
this.dockerContext.logContextInfo();
this.dockerContext.logRootlessWarnings();
// Load project info
try {
const projectinfoInstance = new plugins.projectinfo.ProjectInfo(paths.cwd);
@@ -132,12 +143,30 @@ export class TsDockerManager {
}
// Check if buildx is needed
if (options?.platform || (this.config.platforms && this.config.platforms.length > 1)) {
const useBuildx = !!(options?.platform || (this.config.platforms && this.config.platforms.length > 1));
if (useBuildx) {
await this.ensureBuildx();
}
logger.log('info', '');
logger.log('info', '=== BUILD PHASE ===');
if (useBuildx) {
const platforms = options?.platform || this.config.platforms!.join(', ');
logger.log('info', `Build mode: buildx multi-platform [${platforms}]`);
} else {
logger.log('info', 'Build mode: standard docker build');
}
const localDeps = toBuild.filter(df => df.localBaseImageDependent);
if (localDeps.length > 0) {
logger.log('info', `Local dependencies: ${localDeps.map(df => `${df.cleanTag} -> ${df.localBaseDockerfile?.cleanTag}`).join(', ')}`);
}
if (options?.noCache) {
logger.log('info', 'Cache: disabled (--no-cache)');
}
logger.log('info', `Building ${toBuild.length} Dockerfile(s)...`);
if (options?.cached) {
@@ -151,7 +180,7 @@ export class TsDockerManager {
const useRegistry = Dockerfile.needsLocalRegistry(toBuild, options);
if (useRegistry) {
await Dockerfile.startLocalRegistry();
await Dockerfile.startLocalRegistry(this.dockerContext.contextInfo?.isRootless);
}
try {
@@ -207,6 +236,7 @@ export class TsDockerManager {
timeout: options?.timeout,
noCache: options?.noCache,
verbose: options?.verbose,
isRootless: this.dockerContext.contextInfo?.isRootless,
});
}
@@ -236,29 +266,32 @@ export class TsDockerManager {
* Ensures Docker buildx is set up for multi-architecture builds
*/
private async ensureBuildx(): Promise<void> {
logger.log('info', 'Setting up Docker buildx for multi-platform builds...');
const inspectResult = await smartshellInstance.exec('docker buildx inspect tsdocker-builder 2>/dev/null');
const builderName = this.dockerContext.getBuilderName();
const platforms = this.config.platforms?.join(', ') || 'default';
logger.log('info', `Setting up Docker buildx [${platforms}]...`);
logger.log('info', `Builder: ${builderName}`);
const inspectResult = await smartshellInstance.exec(`docker buildx inspect ${builderName} 2>/dev/null`);
if (inspectResult.exitCode !== 0) {
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'
`docker buildx create --name ${builderName} --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 rm ${builderName} 2>/dev/null`);
await smartshellInstance.exec(
'docker buildx create --name tsdocker-builder --driver docker-container --driver-opt network=host --use'
`docker buildx create --name ${builderName} --driver docker-container --driver-opt network=host --use`
);
await smartshellInstance.exec('docker buildx inspect --bootstrap');
} else {
await smartshellInstance.exec('docker buildx use tsdocker-builder');
await smartshellInstance.exec(`docker buildx use ${builderName}`);
}
}
logger.log('ok', 'Docker buildx ready');
logger.log('ok', `Docker buildx ready (builder: ${builderName}, platforms: ${platforms})`);
}
/**

View File

@@ -79,6 +79,7 @@ export interface IBuildCommandOptions {
noCache?: boolean; // Force rebuild without Docker layer cache (--no-cache)
cached?: boolean; // Skip builds when Dockerfile content hasn't changed
verbose?: boolean; // Stream raw docker build output (default: silent)
context?: string; // Explicit Docker context name (--context flag)
}
export interface ICacheEntry {
@@ -92,3 +93,10 @@ export interface ICacheData {
version: 1;
entries: { [cleanTag: string]: ICacheEntry };
}
export interface IDockerContextInfo {
name: string; // 'default', 'rootless', 'colima', etc.
endpoint: string; // 'unix:///var/run/docker.sock'
isRootless: boolean;
dockerHost?: string; // value of DOCKER_HOST env var, if set
}

View File

@@ -33,7 +33,7 @@ export let run = () => {
try {
const config = await ConfigModule.run();
const manager = new TsDockerManager(config);
await manager.prepare();
await manager.prepare(argvArg.context as string | undefined);
const buildOptions: IBuildCommandOptions = {};
const patterns = argvArg._.slice(1) as string[];
@@ -72,7 +72,7 @@ export let run = () => {
try {
const config = await ConfigModule.run();
const manager = new TsDockerManager(config);
await manager.prepare();
await manager.prepare(argvArg.context as string | undefined);
// Login first
await manager.login();
@@ -124,7 +124,7 @@ export let run = () => {
const config = await ConfigModule.run();
const manager = new TsDockerManager(config);
await manager.prepare();
await manager.prepare(argvArg.context as string | undefined);
// Login first
await manager.login();
@@ -144,7 +144,7 @@ export let run = () => {
try {
const config = await ConfigModule.run();
const manager = new TsDockerManager(config);
await manager.prepare();
await manager.prepare(argvArg.context as string | undefined);
// Build images first
const buildOptions: IBuildCommandOptions = {};
@@ -175,7 +175,7 @@ export let run = () => {
try {
const config = await ConfigModule.run();
const manager = new TsDockerManager(config);
await manager.prepare();
await manager.prepare(argvArg.context as string | undefined);
await manager.login();
logger.log('success', 'Login completed successfully');
} catch (err) {
@@ -191,7 +191,7 @@ export let run = () => {
try {
const config = await ConfigModule.run();
const manager = new TsDockerManager(config);
await manager.prepare();
await manager.prepare(argvArg.context as string | undefined);
await manager.list();
} catch (err) {
logger.log('error', `List failed: ${(err as Error).message}`);