Files
docker/readme.hints.md

12 KiB
Raw Blame History

Docker Module - Development Hints

New Features (2025-11-25 - v5.1.0)

1. Enhanced Network Creation with Full Configuration Support

Problem: Users were unable to create non-overlay networks or customize network configuration. The INetworkCreationDescriptor interface only had a Name property, and DockerNetwork._create() hardcoded Driver: 'overlay'.

Solution: Expanded the interface and implementation to support all Docker network configuration options:

// New interface properties:
export interface INetworkCreationDescriptor {
  Name: string;
  Driver?: 'bridge' | 'overlay' | 'host' | 'none' | 'macvlan';  // NEW
  Attachable?: boolean;  // NEW
  Labels?: Record<string, string>;  // NEW
  IPAM?: {  // NEW - IP Address Management
    Driver?: string;
    Config?: Array<{
      Subnet?: string;
      Gateway?: string;
      IPRange?: string;
      AuxiliaryAddresses?: Record<string, string>;
    }>;
  };
  Internal?: boolean;  // NEW
  EnableIPv6?: boolean;  // NEW
}

Usage Example:

// Create bridge network with custom IPAM
const network = await docker.createNetwork({
  Name: 'custom-bridge',
  Driver: 'bridge',
  IPAM: {
    Config: [{
      Subnet: '172.20.0.0/16',
      Gateway: '172.20.0.1',
    }]
  },
  Labels: { environment: 'production' },
});

Files Modified:

  • ts/interfaces/network.ts - Added all missing properties to interface
  • ts/classes.network.ts - Updated _create() to pass through descriptor properties instead of hardcoding

2. Docker Daemon Version Information

Added: dockerHost.getVersion() method to retrieve Docker daemon version information.

Purpose: Essential for API compatibility checking, debugging, and ensuring minimum Docker version requirements.

Returns:

{
  Version: string;         // e.g., "20.10.21"
  ApiVersion: string;      // e.g., "1.41"
  MinAPIVersion?: string;  // Minimum supported API version
  GitCommit: string;
  GoVersion: string;
  Os: string;              // e.g., "linux"
  Arch: string;            // e.g., "amd64"
  KernelVersion: string;
  BuildTime?: string;
}

Usage Example:

const version = await docker.getVersion();
console.log(`Docker ${version.Version} (API ${version.ApiVersion})`);
console.log(`Platform: ${version.Os}/${version.Arch}`);

Files Modified:

  • ts/classes.host.ts - Added getVersion() method after ping()

3. Image Pruning for Disk Space Management

Added: dockerHost.pruneImages(options?) method to clean up unused images.

Purpose: Automated disk space management, CI/CD cleanup, scheduled maintenance tasks.

Options:

{
  dangling?: boolean;  // Remove untagged images
  filters?: Record<string, string[]>;  // Custom filters (until, label, etc.)
}

Returns:

{
  ImagesDeleted: Array<{ Untagged?: string; Deleted?: string }>;
  SpaceReclaimed: number;  // Bytes freed
}

Usage Example:

// Remove dangling images
const result = await docker.pruneImages({ dangling: true });
console.log(`Reclaimed: ${(result.SpaceReclaimed / 1024 / 1024).toFixed(2)} MB`);

// Remove old images (older than 7 days)
await docker.pruneImages({
  filters: {
    until: ['168h']
  }
});

Files Modified:

  • ts/classes.host.ts - Added pruneImages() method with filter support

4. Exec Command Exit Codes and Inspection

Problem: Users could not determine if exec commands succeeded or failed. The container.exec() method returned a stream but provided no way to access exit codes, which are essential for:

  • Health checks (e.g., pg_isready exit code)
  • Test automation (npm test success/failure)
  • Deployment validation (migration checks)
  • Container readiness probes

Solution: Added inspect() method to exec() return value that provides comprehensive execution information.

New Return Type:

{
  stream: Duplex;
  close: () => Promise<void>;
  inspect: () => Promise<IExecInspectInfo>;  // NEW
}

IExecInspectInfo Interface:

export interface IExecInspectInfo {
  ExitCode: number;        // 0 = success, non-zero = failure
  Running: boolean;        // Whether exec is still running
  Pid: number;             // Process ID
  ContainerID: string;     // Container where exec ran
  ID: string;              // Exec instance ID
  OpenStderr: boolean;
  OpenStdin: boolean;
  OpenStdout: boolean;
  CanRemove: boolean;
  DetachKeys: string;
  ProcessConfig: {
    tty: boolean;
    entrypoint: string;
    arguments: string[];
    privileged: boolean;
  };
}

Usage Example:

// Health check with exit code
const { stream, close, inspect } = await container.exec('pg_isready -U postgres');

stream.on('end', async () => {
  const info = await inspect();

  if (info.ExitCode === 0) {
    console.log('✅ Database is ready');
  } else {
    console.log(`❌ Database check failed (exit code ${info.ExitCode})`);
  }

  await close();
});

Real-World Use Cases Enabled:

  • Health checks: Verify service readiness with proper exit code handling
  • Test automation: Run tests in container and determine pass/fail
  • Deployment validation: Execute migration checks and verify success
  • CI/CD pipelines: Run build/test commands and get accurate results

Files Modified:

  • ts/interfaces/container.ts - Added IExecInspectInfo interface
  • ts/classes.container.ts - Updated exec() return type and added inspect() implementation

Implementation Notes

All changes are non-breaking additions that enhance existing functionality:

  • Network creation: New optional properties with sensible defaults
  • getVersion(): New method, no changes to existing APIs
  • pruneImages(): New method, no changes to existing APIs
  • exec() inspect(): Added to return value, existing stream/close properties unchanged

getContainerById() Bug Fix (2025-11-24 - v5.0.1)

Problem

The getContainerById() method had a critical bug where it would create a DockerContainer object from Docker API error responses when a container didn't exist.

Symptoms:

  • Calling docker.getContainerById('invalid-id') returned a DockerContainer object with { message: "No such container: invalid-id" }
  • Calling .logs() on this invalid container returned "[object Object]" instead of logs or throwing an error
  • No way to detect the error state without checking for a .message property

Root Cause: The DockerContainer._fromId() method made a direct API call to /containers/{id}/json and blindly passed response.body to the constructor, even when the API returned a 404 error response.

Solution

Changed DockerContainer._fromId() to use the list+filter pattern, matching the behavior of all other resource getter methods (DockerImage, DockerNetwork, DockerService, DockerSecret):

// Before (buggy):
public static async _fromId(dockerHostArg: DockerHost, containerId: string): Promise<DockerContainer> {
  const response = await dockerHostArg.request('GET', `/containers/${containerId}/json`);
  return new DockerContainer(dockerHostArg, response.body); // Creates invalid object from error!
}

// After (fixed):
public static async _fromId(dockerHostArg: DockerHost, containerId: string): Promise<DockerContainer | undefined> {
  const containers = await this._list(dockerHostArg);
  return containers.find((container) => container.Id === containerId); // Returns undefined if not found
}

Benefits:

  • 100% consistent with all other resource classes
  • Type-safe return signature: Promise<DockerContainer | undefined>
  • Cannot create invalid objects - .find() naturally returns undefined
  • Users can now properly check for non-existent containers

Usage:

const container = await docker.getContainerById('abc123');
if (container) {
  const logs = await container.logs();
  console.log(logs);
} else {
  console.log('Container not found');
}

OOP Refactoring - Clean Architecture (2025-11-24)

Architecture Changes

The module has been restructured to follow a clean OOP Facade pattern:

  • DockerHost is now the single entry point for all Docker operations
  • All resource classes extend abstract DockerResource base class
  • Static methods are prefixed with _ to indicate internal use
  • Public API is exclusively through DockerHost methods

Key Changes

1. Factory Pattern

  • All resource creation/retrieval goes through DockerHost:
    // Old (deprecated):
    const container = await DockerContainer.getContainers(dockerHost);
    const network = await DockerNetwork.createNetwork(dockerHost, descriptor);
    
    // New (clean API):
    const containers = await dockerHost.listContainers();
    const network = await dockerHost.createNetwork(descriptor);
    

2. Container Management Methods Added The DockerContainer class now has full CRUD and streaming operations:

Lifecycle:

  • container.start() - Start container
  • container.stop(options?) - Stop container
  • container.remove(options?) - Remove container
  • container.refresh() - Reload state

Information:

  • container.inspect() - Get detailed info
  • container.logs(options) - Get logs as string (one-shot)
  • container.stats(options) - Get stats

Streaming & Interactive:

  • container.streamLogs(options) - Stream logs continuously (follow mode)
  • container.attach(options) - Attach to main process (PID 1) with bidirectional stream
  • container.exec(command, options) - Execute commands in container interactively

Example - Stream Logs:

const container = await dockerHost.getContainerById('abc123');
const logStream = await container.streamLogs({ timestamps: true });

logStream.on('data', (chunk) => {
  console.log(chunk.toString());
});

Example - Attach to Container:

const { stream, close } = await container.attach({
  stdin: true,
  stdout: true,
  stderr: true
});

// Pipe to/from process
process.stdin.pipe(stream);
stream.pipe(process.stdout);

// Later: detach
await close();

Example - Execute Command:

const { stream, close } = await container.exec('ls -la /app', {
  tty: true
});

stream.on('data', (chunk) => {
  console.log(chunk.toString());
});

stream.on('end', async () => {
  await close();
});

3. DockerResource Base Class All resource classes now extend DockerResource:

  • Consistent dockerHost property (not dockerHostRef)
  • Required refresh() method
  • Standardized constructor pattern

4. ImageStore Encapsulation

  • dockerHost.imageStore is now private
  • Use dockerHost.storeImage(name, stream) instead
  • Use dockerHost.retrieveImage(name) instead

5. Creation Descriptors Support Both Primitives and Instances Interfaces now accept both strings and class instances:

// Both work:
await dockerHost.createService({
  image: 'nginx:latest',  // String
  networks: ['my-network'],  // String array
  secrets: ['my-secret']  // String array
});

await dockerHost.createService({
  image: imageInstance,  // DockerImage instance
  networks: [networkInstance],  // DockerNetwork array
  secrets: [secretInstance]  // DockerSecret array
});

Migration Guide

Replace all static method calls with dockerHost methods:

  • DockerContainer.getContainers(host)dockerHost.listContainers()
  • DockerImage.createFromRegistry(host, opts)dockerHost.createImageFromRegistry(opts)
  • DockerService.createService(host, desc)dockerHost.createService(desc)
  • dockerHost.imageStore.storeImage(...)dockerHost.storeImage(...)

smartrequest v5+ Migration (2025-11-17)

Breaking Change

smartrequest v5.0.0+ returns web ReadableStream objects (Web Streams API) instead of Node.js streams.

Solution Implemented

All streaming methods now convert web ReadableStreams to Node.js streams using:

plugins.smartstream.nodewebhelpers.convertWebReadableToNodeReadable(webStream)

Files Modified

  • ts/classes.host.ts:

    • requestStreaming() - Converts web stream to Node.js stream before returning
    • getEventObservable() - Works with converted Node.js stream
  • ts/classes.image.ts:

    • createFromTarStream() - Uses converted Node.js stream for event handling
    • exportToTarStream() - Uses converted Node.js stream for backpressure management

Testing

  • Build:  All 11 type errors resolved
  • Tests:  Node.js tests pass (DockerHost, DockerContainer, DockerImage, DockerImageStore)

Notes

  • The conversion maintains backward compatibility with existing code expecting Node.js stream methods (.on(), .emit(), .pause(), .resume())
  • smartstream's nodewebhelpers module provides bidirectional conversion utilities between web and Node.js streams