import Docker from 'dockerode';
import { platform } from 'os';
export interface DockerClientConfig {
socketPath?: string;
host?: string;
port?: number;
protocol?: 'http' | 'https';
ca?: string;
cert?: string;
key?: string;
}
export class DockerClient {
private docker: Docker;
constructor(config?: DockerClientConfig) {
// Support environment variables for Docker connection
const host = config?.host || process.env.DOCKER_HOST;
const port = config?.port || (process.env.DOCKER_PORT ? parseInt(process.env.DOCKER_PORT) : undefined);
const protocol = config?.protocol || (process.env.DOCKER_PROTOCOL as 'http' | 'https') || 'https';
if (host) {
// Remote Docker host (TCP connection)
const dockerOptions: Docker.DockerOptions = {
host: host,
port: port || 2376,
protocol: protocol,
};
// TLS configuration
if (process.env.DOCKER_TLS_VERIFY !== '0' && process.env.DOCKER_TLS_VERIFY !== 'false') {
const certPath = process.env.DOCKER_CERT_PATH || this.getDefaultCertPath();
dockerOptions.ca = config?.ca || this.readFileIfExists(`${certPath}/ca.pem`);
dockerOptions.cert = config?.cert || this.readFileIfExists(`${certPath}/cert.pem`);
dockerOptions.key = config?.key || this.readFileIfExists(`${certPath}/key.pem`);
}
this.docker = new Docker(dockerOptions);
} else {
// Local Docker socket - platform-specific defaults
let socketPath = config?.socketPath || process.env.DOCKER_SOCKET_PATH || this.getDefaultSocketPath();
// Handle Windows named pipe format
// dockerode accepts both '//./pipe/docker_engine' and 'npipe:////./pipe/docker_engine'
if (socketPath.startsWith('npipe://')) {
// dockerode will handle npipe:// protocol
this.docker = new Docker({ socketPath });
} else if (platform() === 'win32' && socketPath.startsWith('//./pipe/')) {
// Windows named pipe - dockerode handles this automatically
this.docker = new Docker({ socketPath });
} else {
// Unix socket (Linux/macOS)
this.docker = new Docker({ socketPath });
}
}
}
private getDefaultSocketPath(): string {
const osPlatform = platform();
switch (osPlatform) {
case 'win32':
// Windows: Docker Desktop uses named pipe
// dockerode supports npipe:// protocol or socketPath with //./pipe/docker_engine
// Try npipe:// first (dockerode's preferred format)
if (process.env.DOCKER_HOST && process.env.DOCKER_HOST.startsWith('npipe://')) {
return process.env.DOCKER_HOST;
}
// Default Windows named pipe path
return '//./pipe/docker_engine';
case 'darwin':
// macOS: Docker Desktop uses /var/run/docker.sock
// But it might also be in ~/.docker/run/docker.sock for newer versions
const macSocket = '/var/run/docker.sock';
if (this.fileExists(macSocket)) {
return macSocket;
}
// Try user-level socket (Docker Desktop 4.0+)
const userSocket = `${process.env.HOME}/.docker/run/docker.sock`;
if (this.fileExists(userSocket)) {
return userSocket;
}
return macSocket; // Default fallback
case 'linux':
default:
// Linux: Standard Unix socket
return '/var/run/docker.sock';
}
}
private getDefaultCertPath(): string {
const osPlatform = platform();
if (osPlatform === 'win32') {
// Windows: Use USERPROFILE instead of HOME
return process.env.USERPROFILE
? `${process.env.USERPROFILE}\\.docker`
: `${process.env.HOME || 'C:\\Users\\'}\\.docker`;
}
// macOS and Linux
return `${process.env.HOME || '~'}/.docker`;
}
private fileExists(path: string): boolean {
try {
const fs = require('fs');
return fs.existsSync(path);
} catch {
return false;
}
}
private readFileIfExists(path: string): string | undefined {
try {
const fs = require('fs');
if (fs.existsSync(path)) {
return fs.readFileSync(path, 'utf8');
}
} catch {
// Ignore errors
}
return undefined;
}
getDocker(): Docker {
return this.docker;
}
async testConnection(): Promise<boolean> {
try {
await this.docker.ping();
return true;
} catch (error) {
return false;
}
}
async getInfo(): Promise<any> {
return await this.docker.info();
}
async getVersion(): Promise<any> {
return await this.docker.version();
}
}