Skip to content

Repository files navigation

@pruefertit/docker-orchestrator

A TypeScript-first Docker orchestration library for programmatic container lifecycle management, monitoring, networking, and resilient operations.

NPM Version CI Status License: MIT Node.js Docker npm downloads TypeScript


Table of Contents


Features

  • Container Lifecycle Management — Create, Start, Stop, Remove, Recreate with automatic rollback
  • Real-Time Log Streaming — stdout/stderr separation, follow mode, tail, time-range filtering
  • Container Metrics — CPU, Memory, Network I/O, Block I/O with continuous streaming
  • Docker Event System — Subscribe to typed events with filtering and auto-reconnect
  • Command Execution — Run commands in containers: simple exec, interactive TTY, script execution
  • Attach/STDIN Streaming — Low-level container attach with stdin/stdout/stderr, fire-and-forget commands
  • Persistent Console — Interactive container console with reconnect, output buffering, sendAndWait, and command queue
  • Preset System — Reusable container configurations with merge logic, graceful stop hooks, and ready-check integration
  • Bidirectional File Transfer — Copy files and buffers between host and container
  • Network Management — Custom bridge/overlay/macvlan networks, DNS aliases, fixed IPs
  • Volume Management — Named volumes, bind mounts, tmpfs with automatic creation
  • Flexible Port Mapping — String/number/object syntax, ranges, UDP, auto-assign with availability checks
  • Resource Limits — Memory (hard/soft), CPU (cores/shares), PID limits, Block I/O weights
  • Security Profiles — Presets (hardened/standard/permissive), capabilities, read-only FS, seccomp
  • Health Checks — HTTP, TCP, and exec-based checks with configurable intervals and timeouts
  • Batch Operations — Parallel deploy/destroy/update with concurrency control and partial-failure handling
  • Resilience — Retry with exponential backoff, circuit breaker, stream recovery, graceful shutdown
  • Stack Deployment — Multi-container stacks with dependency ordering and service scaling
  • Fully Typed — TypeScript-first with Zod schema validation, 200+ exported types

Prerequisites

Requirement Version
Node.js ≥ 18 (recommended: 20)
Docker Engine ≥ 20.10 (API v1.41+)
Platform Linux (primarily supported)

Docker socket access (/var/run/docker.sock) must be available. The library auto-detects the default socket.

Security Notice: Access to the Docker socket is functionally equivalent to root access on the host system. Ensure that only trusted processes are granted access.


Installation

# npm
npm install @pruefertit/docker-orchestrator

# yarn
yarn add @pruefertit/docker-orchestrator

# pnpm
pnpm add @pruefertit/docker-orchestrator

The library has the following runtime dependencies:

  • dockerode — Docker Engine API client
  • eventemitter3 — Event emitter
  • tar-stream — TAR streaming for file transfer
  • zod — Schema validation

Quick Start

import { createClient, createOrchestrator } from "@pruefertit/docker-orchestrator";

// 1. Create a Docker client (auto-detects socket)
const { docker } = await createClient();

// 2. Initialize the orchestrator
const orch = createOrchestrator(docker);

// 3. Deploy a container
const result = await orch.deploy({
  image: "nginx",
  name: "my-webserver",
  portMappings: ["8080:80"],
});

console.log(`Container ${result.name} is running (${result.containerId})`);
console.log(`Ports:`, result.ports);

// 4. Destroy the container
await orch.destroy(result.containerId);

Usage Examples

1. Container Lifecycle

Create and start a container

import { createClient, createOrchestrator } from "@pruefertit/docker-orchestrator";

const { docker } = await createClient();
const orch = createOrchestrator(docker);

const result = await orch.deploy({
  image: "nginx",
  tag: "alpine",
  name: "web",
  env: { NODE_ENV: "production" },
  portMappings: ["8080:80"],
});

console.log(`Status: ${result.status}`); // "running" or "healthy"

Stop a container with timeout

// Graceful stop with 30-second timeout
await orch.destroy(containerId, { timeout: 30 });

Remove a container (normal and force)

// Normal: graceful stop, then remove
await orch.destroy(containerId);

// Force: immediate kill + remove + delete volumes
await orch.destroy(containerId, { force: true, removeVolumes: true });

Inspect container state

import { inspectContainer } from "@pruefertit/docker-orchestrator";

const info = await inspectContainer(docker, containerId);
console.log(`State: ${info.state.status}`);    // running, exited, paused, ...
console.log(`Image: ${info.image}`);
console.log(`Created: ${info.created}`);

List all running containers

import { listContainers } from "@pruefertit/docker-orchestrator";

// Running containers only
const running = await listContainers(docker);

// All containers (including stopped)
const all = await listContainers(docker, { all: true });

for (const c of all) {
  console.log(`${c.name} (${c.id.substring(0, 12)}) — ${c.state}`);
}

2. Configuration

Minimal config (image only)

await orch.deploy({ image: "alpine" });

Set environment variables

await orch.deploy({
  image: "node",
  tag: "20-alpine",
  env: {
    NODE_ENV: "production",
    DATABASE_URL: "postgres://db:5432/app",
    LOG_LEVEL: "info",
  },
});

Port mapping (all variants)

await orch.deploy({
  image: "nginx",
  portMappings: [
    8080,                        // Same host and container port: 0.0.0.0:8080:8080/tcp
    "8080:80",                   // Host:Container
    "127.0.0.1:8080:80",        // With interface binding
    "8080:80/udp",               // UDP protocol
    {                            // Object syntax
      host: 9090,
      container: 80,
      protocol: "tcp",
      ip: "0.0.0.0",
    },
  ],
});

Port availability is automatically checked:

import { checkPortAvailable } from "@pruefertit/docker-orchestrator";

const available = await checkPortAvailable(8080);
console.log(`Port 8080 available: ${available}`);

Volume mounts (all variants)

await orch.deploy({
  image: "postgres",
  mounts: [
    "/host/data:/var/lib/postgresql/data",         // Bind mount
    "pgdata:/var/lib/postgresql/data",             // Named volume
    "/host/config:/etc/config:ro",                 // Read-only
    {                                              // Object syntax
      type: "bind",
      source: "/host/logs",
      target: "/var/log/app",
      readOnly: false,
    },
    {                                              // tmpfs
      type: "tmpfs",
      source: "",
      target: "/tmp",
      tmpfsSize: 100 * 1024 * 1024,               // 100 MB
    },
  ],
});

Labels, working directory, and entrypoint

await orch.deploy({
  image: "node",
  tag: "20-alpine",
  labels: {
    "app.name": "my-service",
    "app.version": "2.1.0",
    "app.team": "backend",
  },
  workingDir: "/app",
  entrypoint: ["node"],
  cmd: ["server.js"],
});

3. Monitoring

Fetch logs (last N lines)

import { tailLogs } from "@pruefertit/docker-orchestrator";

const entries = await tailLogs(docker, containerId, 100);
for (const entry of entries) {
  console.log(`[${entry.stream}] ${entry.message}`);
}

Start and stop a live log stream

import { streamLogs } from "@pruefertit/docker-orchestrator";

const logStream = await streamLogs(docker, containerId, (entry) => {
  const prefix = entry.stream === "stderr" ? "ERR" : "OUT";
  console.log(`[${prefix}] ${entry.message}`);
});

// Stop the stream after 60 seconds
setTimeout(() => logStream.stop(), 60_000);

Handle stdout and stderr separately

import { getContainerLogs } from "@pruefertit/docker-orchestrator";

const stream = await getContainerLogs(docker, containerId, {
  follow: true,
  stdout: true,
  stderr: true,
});

if ("on" in stream) {
  stream.on("data", (entry) => {
    if (entry.stream === "stderr") {
      process.stderr.write(entry.message + "\n");
    } else {
      process.stdout.write(entry.message + "\n");
    }
  });
}

Filter logs since a point in time

import { getContainerLogs } from "@pruefertit/docker-orchestrator";

const entries = await getContainerLogs(docker, containerId, {
  since: new Date("2025-01-01T00:00:00Z"),
  timestamps: true,
  tail: 500,
});

Fetch one-time metrics

import { getMetrics } from "@pruefertit/docker-orchestrator";

const metrics = await getMetrics(docker, containerId);

console.log(`CPU: ${metrics.cpu.percent.toFixed(2)}% (${metrics.cpu.cores} cores)`);
console.log(`RAM: ${(metrics.memory.usedBytes / 1024 / 1024).toFixed(1)} MB / ${(metrics.memory.limitBytes / 1024 / 1024).toFixed(1)} MB (${metrics.memory.percent.toFixed(1)}%)`);
console.log(`Net RX: ${metrics.network.rxBytes} bytes, TX: ${metrics.network.txBytes} bytes`);
console.log(`Disk Read: ${metrics.blockIO.readBytes} bytes, Write: ${metrics.blockIO.writeBytes} bytes`);

Start a continuous metrics stream

import { streamMetrics } from "@pruefertit/docker-orchestrator";

const metricsStream = await streamMetrics(docker, containerId, 5000); // Every 5 seconds

metricsStream.on("data", (metrics) => {
  console.log(`CPU: ${metrics.cpu.percent.toFixed(1)}% | RAM: ${metrics.memory.percent.toFixed(1)}%`);
});

metricsStream.on("error", (err) => console.error("Metrics error:", err));

// Stop later
metricsStream.stop();

Subscribe to and filter Docker events

import { subscribeEvents } from "@pruefertit/docker-orchestrator";

const subscription = await subscribeEvents(docker, {
  type: "container",
  action: ["start", "stop", "die"],
});

subscription.on("container.start", (event) => {
  console.log(`Container started: ${event.actor.name}`);
});

subscription.on("container.die", (event) => {
  console.log(`Container died: ${event.actor.name} (Exit: ${event.actor.attributes.exitCode})`);
});

// All events
subscription.on("event", (event) => {
  console.log(`${event.type}.${event.action}: ${event.actor.id.substring(0, 12)}`);
});

// Unsubscribe
subscription.unsubscribe();

4. Exec & Files

Run a simple command and read output

import { executeCommand } from "@pruefertit/docker-orchestrator";

const result = await executeCommand(docker, containerId, "ls -la /app");
console.log("stdout:", result.stdout);
console.log("stderr:", result.stderr);
console.log("Exit code:", result.exitCode);

Evaluate exit code

const result = await executeCommand(docker, containerId, "test -f /app/config.json");

if (result.exitCode === 0) {
  console.log("Config file exists");
} else {
  console.log("Config file missing");
}

Command with env vars and working directory

const result = await executeCommand(docker, containerId, "node migrate.js", {
  env: ["DATABASE_URL=postgres://localhost:5432/app"],
  workingDir: "/app",
  user: "node",
  timeout: 60_000,
});

Interactive shell session

import { executeInteractive } from "@pruefertit/docker-orchestrator";

const handle = await executeInteractive(docker, containerId, "/bin/bash", {
  tty: true,
});

handle.stdout.on("data", (chunk) => process.stdout.write(chunk));
process.stdin.pipe(handle.stdin);

// Resize terminal
await handle.resize(120, 40);

Run a script inside a container

import { executeScript } from "@pruefertit/docker-orchestrator";

// Run a local script inside the container
const result = await executeScript(docker, containerId, "/local/scripts/setup.sh");
console.log("Output:", result.stdout);

Copy a file from host to container

import { copyToContainer } from "@pruefertit/docker-orchestrator";

await copyToContainer(docker, containerId, {
  sourcePath: "/local/app/config.json",
  destPath: "/app/config.json",
});

Copy a file from container to host

import { copyFromContainer } from "@pruefertit/docker-orchestrator";

await copyFromContainer(docker, containerId, {
  sourcePath: "/app/data/export.csv",
  destPath: "/local/exports/export.csv",
});

Write a config file as a string into a container

import { copyBufferToContainer } from "@pruefertit/docker-orchestrator";

const configContent = JSON.stringify({
  database: { host: "db", port: 5432 },
  redis: { host: "cache", port: 6379 },
}, null, 2);

await copyBufferToContainer(
  docker,
  containerId,
  "/app/config.json",
  Buffer.from(configContent),
);

5. Networking

Create a custom network

import { createNetwork, removeNetwork } from "@pruefertit/docker-orchestrator";

const network = await createNetwork(docker, {
  name: "app-network",
  driver: "bridge",
  subnet: "172.20.0.0/16",
  gateway: "172.20.0.1",
  labels: { environment: "production" },
});

console.log(`Network created: ${network.id}`);

Connect a container to a network with a DNS alias

import { connectContainer } from "@pruefertit/docker-orchestrator";

await connectContainer(docker, "app-network", containerId, {
  aliases: ["web", "frontend"],
});

Two containers communicating over a network

const network = await createNetwork(docker, {
  name: "backend-net",
  driver: "bridge",
});

const db = await orch.deploy({
  image: "postgres",
  tag: "16-alpine",
  name: "db",
  networks: {
    "backend-net": { aliases: ["database"] },
  },
  env: { POSTGRES_PASSWORD: "secret" },
});

const app = await orch.deploy({
  image: "node",
  tag: "20-alpine",
  name: "app",
  networks: {
    "backend-net": { aliases: ["app"] },
  },
  env: { DATABASE_URL: "postgres://postgres:secret@database:5432/postgres" },
});

// 'app' can reach 'db' via the DNS alias "database"

Container with a fixed IP

await orch.deploy({
  image: "nginx",
  name: "web-fixed-ip",
  networks: {
    "app-network": {
      ipv4Address: "172.20.0.100",
      aliases: ["web"],
    },
  },
});

Clean up networks

import { pruneNetworks, disconnectContainer, removeNetwork } from "@pruefertit/docker-orchestrator";

// Remove a single network (containers must be disconnected first)
await disconnectContainer(docker, "app-network", containerId);
await removeNetwork(docker, "app-network");

// Remove all unused networks
const pruned = await pruneNetworks(docker);

6. Volumes

Create a named volume

import { createVolume, inspectVolume } from "@pruefertit/docker-orchestrator";

const volume = await createVolume(docker, {
  name: "app-data",
  labels: { app: "my-service" },
});

console.log(`Volume: ${volume.name}, Mountpoint: ${volume.mountpoint}`);

Mount a volume into a container

await orch.deploy({
  image: "postgres",
  name: "db",
  mounts: ["app-data:/var/lib/postgresql/data"],
});

Data persistence across container restarts

// Deploy container with volume
const result = await orch.deploy({
  image: "postgres",
  name: "db-persistent",
  mounts: ["pgdata:/var/lib/postgresql/data"],
  env: { POSTGRES_PASSWORD: "secret" },
});

// Destroy the container (volume is preserved)
await orch.destroy(result.containerId);

// Start a new container with the same volume — data is still there
await orch.deploy({
  image: "postgres",
  name: "db-persistent-new",
  mounts: ["pgdata:/var/lib/postgresql/data"],
  env: { POSTGRES_PASSWORD: "secret" },
});

List and prune volumes

import { listVolumes, pruneVolumes } from "@pruefertit/docker-orchestrator";

const volumes = await listVolumes(docker);
for (const vol of volumes) {
  console.log(`${vol.name} (Driver: ${vol.driver})`);
}

// Remove unused volumes
const pruned = await pruneVolumes(docker);
console.log(`Removed: ${pruned.volumesDeleted.length} volumes, ${pruned.spaceReclaimed} bytes freed`);

7. Resource Limits

Set memory limits (hard + soft)

await orch.deploy({
  image: "node",
  tag: "20-alpine",
  name: "app-limited",
  resources: {
    memory: {
      limit: "512m",         // Hard limit: 512 MB
      reservation: "256m",   // Soft limit: 256 MB
      swap: "1g",            // Swap limit: 1 GB
      swappiness: 60,        // Swap tendency (0-100)
    },
  },
});

Set CPU limits

await orch.deploy({
  image: "python",
  name: "worker",
  resources: {
    cpu: {
      nanoCpus: "1.5",      // 1.5 CPU cores
      shares: 512,           // Relative weight (default: 1024)
      cpusetCpus: "0,1",    // Use only cores 0 and 1
    },
  },
});

Set PID limit

await orch.deploy({
  image: "nginx",
  name: "web-safe",
  resources: {
    pids: {
      limit: 200,            // Max 200 processes (fork bomb protection)
    },
  },
});

Monitor limits with metrics

import { getMetrics } from "@pruefertit/docker-orchestrator";

const metrics = await getMetrics(docker, containerId);
const memPercent = metrics.memory.percent;

if (memPercent > 80) {
  console.warn(`Container is using ${memPercent.toFixed(1)}% of its memory limit!`);
}

8. Security

Use the hardened security preset

await orch.deploy({
  image: "nginx",
  name: "secure-web",
  securityProfile: "hardened",
  // Automatically sets:
  // - user: "1000:1000"
  // - readonlyRootfs: true
  // - autoTmpfs: true (for /tmp, /var/run, etc.)
  // - capDrop: ["ALL"]
  // - noNewPrivileges: true
  // - seccomp: "default"
});

Set a non-root user

await orch.deploy({
  image: "node",
  name: "app",
  security: {
    user: "1000:1000",
    groupAdd: ["audio", "video"],
  },
});

Drop and add capabilities

await orch.deploy({
  image: "nginx",
  name: "web",
  security: {
    capDrop: ["ALL"],
    capAdd: ["NET_BIND_SERVICE"],   // Allow binding to ports below 1024 only
    noNewPrivileges: true,
  },
});

Read-only root filesystem with tmpfs for writable paths

await orch.deploy({
  image: "nginx",
  name: "readonly-web",
  security: {
    readonlyRootfs: true,
    tmpfsMounts: {
      "/tmp": "size=64m",
      "/var/run": "size=16m",
      "/var/cache/nginx": "size=128m",
    },
  },
});

Load a custom seccomp profile

await orch.deploy({
  image: "app",
  name: "custom-seccomp",
  security: {
    seccomp: { profilePath: "/path/to/seccomp-profile.json" },
    noNewPrivileges: true,
  },
});

Combine a preset with individual overrides

// Use the hardened preset as a base, but allow NET_BIND_SERVICE
await orch.deploy({
  image: "nginx",
  name: "hardened-web",
  securityProfile: "hardened",
  security: {
    capAdd: ["NET_BIND_SERVICE"],
    user: "nginx:nginx",           // Override 1000:1000
  },
});

9. Health Checks

HTTP health check

await orch.deploy({
  image: "nginx",
  name: "web-healthy",
  portMappings: ["8080:80"],
  healthCheck: {
    type: "http",
    httpGet: {
      path: "/health",
      port: 80,
      expectedStatus: 200,
    },
    interval: 10,       // Check every 10 seconds
    timeout: 5,          // 5-second timeout per check
    retries: 3,          // 3 failures = unhealthy
    startPeriod: 15,     // 15-second grace period after start
  },
});

TCP health check

await orch.deploy({
  image: "postgres",
  name: "db",
  healthCheck: {
    type: "tcp",
    tcpSocket: { port: 5432 },
    interval: 5,
    timeout: 3,
    retries: 5,
  },
});

Exec health check

await orch.deploy({
  image: "redis",
  name: "cache",
  healthCheck: {
    type: "exec",
    exec: { command: ["redis-cli", "ping"] },
    interval: 10,
    timeout: 5,
    retries: 3,
  },
});

Wait for healthy status after deploy

import { waitForHealthy } from "@pruefertit/docker-orchestrator";

const hcResult = await waitForHealthy(docker, containerId, {
  type: "http",
  httpGet: { path: "/ready", port: 8080 },
  interval: 2,
  timeout: 5,
  retries: 10,
  startPeriod: 5,
});

if (hcResult.status === "healthy") {
  console.log(`Container healthy after ${hcResult.checks} checks (${hcResult.elapsed}ms)`);
} else {
  console.error(`Health check failed: ${hcResult.lastError}`);
}

Timeout handling on health check failure

try {
  const result = await orch.deploy({
    image: "broken-app",
    name: "will-fail",
    healthCheck: {
      type: "http",
      httpGet: { path: "/health", port: 8080 },
      interval: 2,
      timeout: 3,
      retries: 3,
    },
  });
} catch (err) {
  if (err instanceof DeploymentFailedError && err.step === "healthcheck") {
    console.error("Container failed health check — was automatically removed");
  }
}

10. Container Updates

Update a container with a new config (env var change)

const updateResult = await orch.update(containerId, {
  env: { LOG_LEVEL: "debug", FEATURE_FLAG: "true" },
});

console.log(`Changes: ${updateResult.changes.length}`);
console.log(`Restart required: ${updateResult.restarted}`);
for (const change of updateResult.changes) {
  console.log(`  ${change.field}: ${JSON.stringify(change.oldValue)} → ${JSON.stringify(change.newValue)}`);
}

Image update with recreation

// Image changes automatically require a container restart
const updateResult = await orch.update(containerId, {
  image: "nginx",
  tag: "1.27-alpine",
});

console.log(`New container: ${updateResult.containerId}`);
console.log(`Restarted: ${updateResult.restarted}`); // true

Show config diff before updating

import { diffConfigs } from "@pruefertit/docker-orchestrator";

const currentConfig = { image: "nginx", tag: "1.25", env: { A: "1" } };
const newConfig = { image: "nginx", tag: "1.27", env: { A: "2", B: "3" } };

const changes = diffConfigs(currentConfig, newConfig);
for (const change of changes) {
  console.log(`${change.field}: ${JSON.stringify(change.oldValue)} → ${JSON.stringify(change.newValue)}`);
}

11. Batch Operations

Deploy multiple containers in parallel

const batchResult = await orch.deployMany(
  [
    { image: "nginx", name: "web-1", portMappings: ["8081:80"] },
    { image: "nginx", name: "web-2", portMappings: ["8082:80"] },
    { image: "nginx", name: "web-3", portMappings: ["8083:80"] },
  ],
  {
    concurrency: 3,
    onProgress: (step, detail) => console.log(`${step}: ${detail}`),
  },
);

console.log(`Succeeded: ${batchResult.succeeded}, Failed: ${batchResult.failed}`);

Evaluate partial results

for (const item of batchResult.results) {
  if (item.status === "fulfilled") {
    console.log(`[${item.index}] Deployed: ${item.value.containerId}`);
  } else {
    console.error(`[${item.index}] Error: ${item.reason.message}`);
  }
}

Destroy multiple containers in parallel

const destroyResult = await orch.destroyMany(
  [container1Id, container2Id, container3Id],
  { force: true, concurrency: 5 },
);

Configure concurrency limit

// Deploy at most 2 containers simultaneously (conserves resources)
const result = await orch.deployMany(configs, { concurrency: 2 });

12. Stack Deployment

Define a multi-container stack (app + DB)

import { deployStack, destroyStack } from "@pruefertit/docker-orchestrator";

const stackResult = await deployStack(docker, {
  name: "my-app",
  containers: {
    db: {
      image: "postgres",
      tag: "16-alpine",
      env: { POSTGRES_PASSWORD: "secret", POSTGRES_DB: "app" },
      mounts: ["db-data:/var/lib/postgresql/data"],
      healthCheck: {
        type: "tcp",
        tcpSocket: { port: 5432 },
        interval: 5,
        timeout: 3,
        retries: 5,
      },
    },
    app: {
      image: "myapp",
      tag: "latest",
      dependsOn: ["db"],          // DB starts first
      env: { DATABASE_URL: "postgres://postgres:secret@db:5432/app" },
      portMappings: ["3000:3000"],
      healthCheck: {
        type: "http",
        httpGet: { path: "/health", port: 3000 },
        interval: 10,
        timeout: 5,
        retries: 3,
        startPeriod: 10,
      },
    },
  },
  networks: {
    default: { driver: "bridge" },
  },
  volumes: {
    "db-data": {},
  },
});

console.log(`Stack "${stackResult.stackName}" deployed`);
for (const svc of stackResult.services) {
  console.log(`  ${svc.serviceName}: ${svc.deployResults.length} instance(s)`);
}

Destroy a stack

await destroyStack(docker, "my-app");

13. Resilience & Error Handling

Catch errors and check error type

import {
  isDockerOrchestratorError,
  isTransientError,
  ContainerNotFoundError,
  PortAlreadyInUseError,
} from "@pruefertit/docker-orchestrator";

try {
  await orch.deploy({ image: "nginx", portMappings: ["80:80"] });
} catch (err) {
  if (isDockerOrchestratorError(err)) {
    console.error(`Code: ${err.code}, Message: ${err.message}`);
    console.error(`Timestamp: ${err.timestamp}`);
    console.error(`Context:`, err.context);

    if (err instanceof PortAlreadyInUseError) {
      console.error(`Port ${err.port} in use. Suggestion: ${err.suggestedPort}`);
    }

    if (isTransientError(err)) {
      console.log("Transient error — retry possible");
    }
  }
}

Customize retry policy

const orch = createOrchestrator(docker, {
  retryPolicy: {
    imagePull: { maxRetries: 5, initialDelay: 3000 },
    containerStart: { maxRetries: 3, initialDelay: 2000 },
  },
});

Or use the retry function directly:

import { retry } from "@pruefertit/docker-orchestrator";

const result = await retry(
  () => executeCommand(docker, containerId, "curl http://service/api"),
  {
    maxRetries: 5,
    initialDelay: 1000,
    maxDelay: 30_000,
    backoffMultiplier: 2,
    jitter: true,
    retryOn: (err) => isTransientError(err),
    onRetry: (attempt, err, nextDelay) => {
      console.log(`Retry ${attempt}, next attempt in ${nextDelay}ms`);
    },
  },
);

Query circuit breaker status

const health = orch.health();
console.log(`Daemon: ${health.daemon}`);           // "connected" | "disconnected" | "reconnecting"
console.log(`Circuit: ${health.circuit}`);          // "closed" | "open" | "half-open"
console.log(`Pending Ops: ${health.pendingOperations}`);

Standalone circuit breaker:

import { CircuitBreaker } from "@pruefertit/docker-orchestrator";

const breaker = new CircuitBreaker({
  failureThreshold: 5,
  resetTimeout: 30_000,
  halfOpenMaxAttempts: 1,
});

breaker.on("circuit.open", () => console.warn("Circuit opened!"));
breaker.on("circuit.closed", () => console.log("Circuit closed"));

const result = await breaker.execute(() => fetch("http://service/api"));

Handle daemon disconnect/reconnect events

import { DaemonMonitor } from "@pruefertit/docker-orchestrator";

const monitor = new DaemonMonitor(docker, {
  pingInterval: 10_000,
  failureThreshold: 3,
});

monitor.onDaemonDisconnect(() => {
  console.error("Docker daemon unreachable!");
});

monitor.onDaemonReconnect(() => {
  console.log("Docker daemon reconnected");
});

await monitor.start();

// Clean up
monitor.destroy();

Implement graceful shutdown

import { ShutdownManager } from "@pruefertit/docker-orchestrator";

const shutdown = new ShutdownManager({ timeout: 15_000 });

// Register cleanup callbacks
shutdown.register("stop-containers", async () => {
  await orch.destroyMany(containerIds);
});

shutdown.register("close-connections", () => {
  // Close DB connections
});

// Install signal handlers (SIGINT, SIGTERM)
shutdown.installSignalHandlers();

// Or trigger manually
await shutdown.shutdown();

Check orchestrator health

const status = orch.health();

if (status.daemon === "disconnected") {
  console.error("Docker daemon offline");
}

if (status.circuit === "open") {
  console.warn("Circuit breaker open — operations will be rejected");
}

Inject a custom logger

import { createLogger, createOrchestrator } from "@pruefertit/docker-orchestrator";

const logger = createLogger({ level: "debug", json: true });

const orch = createOrchestrator(docker, { logger });

Or implement your own logger:

import type { Logger } from "@pruefertit/docker-orchestrator";

const customLogger: Logger = {
  error: (msg, ctx) => myLoggingSystem.error(msg, ctx),
  warn: (msg, ctx) => myLoggingSystem.warn(msg, ctx),
  info: (msg, ctx) => myLoggingSystem.info(msg, ctx),
  debug: (msg, ctx) => myLoggingSystem.debug(msg, ctx),
  trace: (msg, ctx) => myLoggingSystem.trace(msg, ctx),
};

const orch = createOrchestrator(docker, { logger: customLogger });

14. Advanced Patterns

Run a container as a short-lived job

const job = await orch.deploy({
  image: "node",
  tag: "20-alpine",
  name: "migration-job",
  cmd: ["node", "migrate.js"],
  restartPolicy: "no",
});

// Wait for completion
const container = docker.getContainer(job.containerId);
await container.wait();

// Read output
const logs = await tailLogs(docker, job.containerId, 1000);
console.log(logs.map((e) => e.message).join("\n"));

// Clean up
await orch.destroy(job.containerId, { removeVolumes: true });

Sidecar pattern: main container + log collector

const network = await createNetwork(docker, { name: "sidecar-net", driver: "bridge" });

const app = await orch.deploy({
  image: "myapp",
  name: "app-main",
  networks: { "sidecar-net": { aliases: ["app"] } },
  mounts: ["shared-logs:/var/log/app"],
});

const logCollector = await orch.deploy({
  image: "fluentd",
  name: "log-collector",
  networks: { "sidecar-net": { aliases: ["logs"] } },
  mounts: ["shared-logs:/var/log/app:ro"],
});

Blue-green deployment

// Start the new container (green)
const green = await orch.deploy({
  image: "myapp",
  tag: "v2.0.0",
  name: "app-green",
  portMappings: ["8081:8080"],
  healthCheck: {
    type: "http",
    httpGet: { path: "/health", port: 8080 },
    interval: 2,
    timeout: 5,
    retries: 5,
  },
});

if (green.status === "healthy") {
  // Redirect traffic (update load balancer config)
  console.log("Green is healthy — redirecting traffic");

  // Remove the old container (blue)
  await orch.destroy(blueContainerId);
  console.log("Blue removed — deployment complete");
} else {
  // Rollback: remove green
  await orch.destroy(green.containerId);
  console.error("Green health check failed — rolling back");
}

Periodic task: container for cron-like jobs

async function runPeriodicJob() {
  const job = await orch.deploy({
    image: "myapp",
    tag: "latest",
    name: `cleanup-job-${Date.now()}`,
    cmd: ["node", "cleanup.js"],
    restartPolicy: "no",
    resources: { memory: { limit: "256m" }, cpu: { nanoCpus: "0.5" } },
  });

  const container = docker.getContainer(job.containerId);
  const { StatusCode } = await container.wait();

  if (StatusCode !== 0) {
    const logs = await tailLogs(docker, job.containerId, 50);
    console.error(`Job failed (Exit ${StatusCode}):`, logs.map((e) => e.message).join("\n"));
  }

  await orch.destroy(job.containerId, { removeVolumes: true });
}

// Run every 60 minutes
setInterval(runPeriodicJob, 60 * 60 * 1000);

15. Attach/STDIN & Console

Deploy a container with interactive STDIN

const result = await orch.deploy({
  image: "alpine",
  name: "interactive-shell",
  cmd: ["cat"],
  interactive: true,  // Enables OpenStdin + AttachStdin
});

// Console is automatically available for interactive containers
const cons = result.console;

Low-level attach to a container

import { attachContainer } from "@pruefertit/docker-orchestrator";

const { stream, demuxed, tty } = await attachContainer(docker, containerId);

// Send data
stream.write("hello\n");

// Receive output (non-TTY: demuxed stdout/stderr)
demuxed!.stdout.on("data", (chunk: Buffer) => {
  console.log("stdout:", chunk.toString());
});

demuxed!.stderr.on("data", (chunk: Buffer) => {
  console.error("stderr:", chunk.toString());
});

// Close stream
stream.end();

Send fire-and-forget commands

import { sendCommand, sendCommands } from "@pruefertit/docker-orchestrator";

// Send a single command (no output returned)
await sendCommand(docker, containerId, "start-process");

// Send multiple commands sequentially
await sendCommands(docker, containerId, [
  "config set maxmemory 256mb",
  "config set maxmemory-policy allkeys-lru",
  "save",
], 100); // 100ms pause between commands

Via the orchestrator

// Single command
await orch.attach.send(containerId, "reload-config");

// Multiple commands
await orch.attach.sendMany(containerId, ["cmd1", "cmd2"]);

Create a persistent console

import { createConsole } from "@pruefertit/docker-orchestrator";

const console = await createConsole(docker, containerId, {
  reconnect: true,           // Auto-reconnect on connection loss
  reconnectMaxRetries: 10,   // Max 10 reconnect attempts
  outputBufferSize: 1000,    // Buffer last 1000 lines
  queueCommands: false,      // Queue commands when disconnected
});

// Or via the orchestrator
const console2 = await orch.attach.console(containerId);

Send a command and wait for a response

const result = await console.sendAndWait("status", {
  matchOutput: "Server is running",  // Wait until this text appears
  timeout: 5000,                     // Wait max 5 seconds
});

console.log(`Output: ${result.output}`);
console.log(`Duration: ${result.duration}ms`);

Output buffer and events

// Listen to all events
console.on("output", (line) => {
  console.log(`[${line.stream}] ${line.message}`);
});

console.on("connected", () => console.log("Connected"));
console.on("disconnected", () => console.log("Disconnected"));
console.on("reconnecting", (attempt) => console.log(`Reconnect #${attempt}`));

// Retrieve buffer
const buffer = console.getBuffer();
for (const line of buffer) {
  console.log(`[${line.timestamp.toISOString()}] ${line.message}`);
}

// Clear buffer
console.clearBuffer();

// Query uptime
console.log(`Connected for ${console.uptime}ms`);

// Disconnect
console.disconnect();

Enable TTY mode

await orch.deploy({
  image: "alpine",
  name: "tty-container",
  cmd: ["/bin/sh"],
  interactive: true,
  tty: true,  // Enable TTY (pseudo-terminal)
});

16. Preset System

Define and register a preset

import { definePreset } from "@pruefertit/docker-orchestrator";

const minecraftPreset = definePreset({
  name: "minecraft-server",
  config: {
    image: "itzg/minecraft-server",
    env: {
      EULA: "TRUE",
      TYPE: "PAPER",
      MEMORY: "2G",
    },
    portMappings: ["25565:25565"],
    mounts: ["mc-data:/data"],
    resources: { memory: { limit: "3g" } },
  },
  gracefulStop: {
    command: "stop",         // Command for clean shutdown
    waitForExit: true,       // Wait until container exits
    timeout: 30000,          // Wait max 30 seconds
  },
  readyCheck: {
    logMatch: /Done.*For help/,  // RegExp match on log output
    timeout: 120000,             // Wait max 2 minutes for ready
  },
  metadata: {
    description: "Minecraft Paper Server",
    version: "1.0.0",
  },
});

// Register with the orchestrator
orch.presets.register(minecraftPreset);

Register multiple presets

orch.presets.registerMany([
  definePreset({ name: "redis", config: { image: "redis:alpine", cmd: ["redis-server"] } }),
  definePreset({ name: "postgres", config: { image: "postgres:16-alpine", env: { POSTGRES_PASSWORD: "secret" } } }),
]);

Deploy a container from a preset

const result = await orch.deploy({
  image: "itzg/minecraft-server",
  preset: "minecraft-server",
  name: "mc-survival",
  env: { DIFFICULTY: "hard", MODE: "survival" },  // Merged with preset env
});

// Preset env + user env are merged:
// EULA=TRUE, TYPE=PAPER, MEMORY=2G (from preset)
// DIFFICULTY=hard, MODE=survival (user overrides)

Merge logic for preset + user config

// Preset defines base config
const preset = definePreset({
  name: "web-app",
  config: {
    image: "node:20-alpine",
    env: { PORT: "3000", NODE_ENV: "production" },
    portMappings: ["3000:3000"],
    mounts: ["app-logs:/var/log"],
    labels: { "app.type": "web" },
  },
});

orch.presets.register(preset);

// User deploy merges intelligently:
await orch.deploy({
  image: "node:20-alpine",
  preset: "web-app",
  env: { NODE_ENV: "staging", DEBUG: "true" },      // Key-based: NODE_ENV is overridden, DEBUG is added
  portMappings: ["8080:3000"],                       // User ports override preset ports
  mounts: ["app-data:/data"],                        // Additive: both mounts are active
  labels: { "app.version": "2.0" },                  // Key-based: app.type remains, app.version is added
});

Graceful stop on destroy

// On destroy, the gracefulStop command is sent automatically
await orch.destroy(result.containerId, { timeout: 60 });
// → Sends "stop" to the Minecraft server
// → Waits until container exits cleanly
// → Falls back to force-stop after timeout

Ready check on deploy

// Container is not reported as "running" until the ready check passes
const result = await orch.deploy({
  image: "itzg/minecraft-server",
  preset: "minecraft-server",
  name: "mc-creative",
});

// result.status === "running" only once "Done.*For help" appears in the log

Manage presets

// List all registered presets
const names = orch.presets.list();  // ["minecraft-server", "redis", "postgres"]

// Get a preset
const preset = orch.presets.get("minecraft-server");
console.log(preset.config.image);  // "itzg/minecraft-server"

// Check if preset exists
orch.presets.has("minecraft-server");  // true

// Remove a preset
orch.presets.remove("minecraft-server");

// Remove all presets
orch.presets.clear();

Serialize and load presets (JSON)

import { serializePreset, deserializePreset } from "@pruefertit/docker-orchestrator";

// Serialize preset to JSON (with RegExp support)
const json = serializePreset(minecraftPreset);
// RegExp is stored as "__REGEXP__Done.*For help__FLAGS__"

// Load preset from JSON
const restored = deserializePreset(json);
orch.presets.register(restored);

Overwrite a preset

// By default, register() throws an error on duplicates
try {
  orch.presets.register(definePreset({ name: "redis", config: { image: "redis:7" } }));
} catch (err) {
  // PresetAlreadyExistsError
}

// Allowed with overwrite option
orch.presets.register(
  definePreset({ name: "redis", config: { image: "redis:7" } }),
  { overwrite: true },
);

API Quick Reference

Orchestrator

Method Description Return Type
createOrchestrator(docker, options?) Factory function Orchestrator
orch.deploy(config, onProgress?) Deploy a container Promise<DeployResult>
orch.update(containerId, config, onProgress?) Update a container Promise<UpdateResult>
orch.destroy(containerId, options?) Destroy a container Promise<void>
orch.deployMany(configs, options?) Batch deploy Promise<BatchResult<DeployResult>>
orch.destroyMany(ids, options?) Batch destroy Promise<BatchResult<void>>
orch.updateMany(updates, options?) Batch update Promise<BatchResult<UpdateResult>>
orch.listManagedContainers() List managed containers Promise<Array<{ containerId, name, status, deployedAt }>>
orch.syncState() Sync state with Docker Promise<{ synced: number, orphans: string[] }>
orch.health() Query health status OrchestratorHealthStatus
orch.shutdown() Graceful shutdown Promise<void>
orch.presets Access preset registry PresetRegistry
orch.attach.send(id, cmd) Send fire-and-forget command Promise<void>
orch.attach.sendMany(id, cmds, delay?) Send multiple commands Promise<void>
orch.attach.console(id, options?) Create persistent console Promise<ContainerConsole>

Client & Container

Method Description Return Type
createClient(options?) Create Docker client Promise<CreateClientResult>
createContainer(docker, config) Create container Promise<{ id }>
startContainer(docker, id) Start container Promise<void>
stopContainer(docker, id, timeout?) Stop container Promise<void>
removeContainer(docker, id, options?) Remove container Promise<void>
inspectContainer(docker, id) Inspect container Promise<ContainerInspectResult>
listContainers(docker, options?) List containers Promise<ContainerInfo[]>

Image

Method Description Return Type
imageExists(docker, imageRef) Check if image exists Promise<boolean>
pullImage(docker, imageRef, onProgress?) Pull image Promise<void>
listImages(docker) List images Promise<ImageInfo[]>
removeImage(docker, imageRef) Remove image Promise<void>

Logs & Metrics

Method Description Return Type
getContainerLogs(docker, id, options?) Fetch logs Promise<LogEntry[] | LogStream>
tailLogs(docker, id, lines) Last N log lines Promise<LogEntry[]>
streamLogs(docker, id, onEntry) Live log stream Promise<LogStream>
getMetrics(docker, id) One-time metrics Promise<ContainerMetrics>
streamMetrics(docker, id, intervalMs?) Metrics stream Promise<MetricsStream>

Events

Method Description Return Type
subscribeEvents(docker, filter?) Subscribe to Docker events Promise<EventSubscription>

Exec & Files

Method Description Return Type
executeCommand(docker, id, cmd, options?) Execute command Promise<ExecResult>
executeInteractive(docker, id, cmd, options?) Interactive session Promise<InteractiveExecHandle>
executeScript(docker, id, scriptPath, options?) Execute script Promise<ExecResult>
copyToContainer(docker, id, options) Host → Container Promise<void>
copyFromContainer(docker, id, options) Container → Host Promise<void>
copyBufferToContainer(docker, id, destPath, buffer) Buffer → Container Promise<void>
readFileFromContainer(docker, id, filePath) Read file from container Promise<Buffer>

Networking

Method Description Return Type
createNetwork(docker, options) Create network Promise<NetworkInfo>
removeNetwork(docker, name) Remove network Promise<void>
inspectNetwork(docker, name) Inspect network Promise<NetworkInfo>
listNetworks(docker, filter?) List networks Promise<NetworkInfo[]>
connectContainer(docker, network, id, options?) Connect container Promise<void>
disconnectContainer(docker, network, id) Disconnect container Promise<void>
pruneNetworks(docker) Remove unused networks Promise<string[]>

Volume

Method Description Return Type
createVolume(docker, options) Create volume Promise<VolumeInfo>
removeVolume(docker, name, force?) Remove volume Promise<void>
inspectVolume(docker, name) Inspect volume Promise<VolumeInfo>
listVolumes(docker, filter?) List volumes Promise<VolumeInfo[]>
pruneVolumes(docker) Remove unused volumes Promise<PruneVolumesResult>
volumeExists(docker, name) Check if volume exists Promise<boolean>

Stack

Method Description Return Type
deployStack(docker, config, onProgress?) Deploy stack Promise<StackDeployResult>
destroyStack(docker, stackName) Destroy stack Promise<void>
resolveDependencyOrder(containers) Resolve dependency order string[]

Attach & Console

Method / Class Description Return Type
attachContainer(docker, id, options?) Low-level container attach Promise<AttachResult>
sendCommand(docker, id, command, timeout?) Send single command Promise<void>
sendCommands(docker, id, commands, delay?, timeout?) Send multiple commands Promise<void>
createConsole(docker, id, options?) Create persistent console Promise<ContainerConsole>
ContainerConsole Interactive container console Class
console.connect() Connect console Promise<void>
console.disconnect() Disconnect console void
console.send(command) Send command void
console.sendAndWait(command, options?) Send command and wait for response Promise<SendAndWaitResult>
console.getBuffer() Get output buffer ConsoleOutputLine[]
console.clearBuffer() Clear output buffer void

Presets

Method / Class Description Return Type
definePreset(input) Define preset with validation ContainerPreset
serializePreset(preset) Serialize preset to JSON string
deserializePreset(json) Load preset from JSON ContainerPreset
mergePresetConfig(presetConfig, userOverrides) Merge preset and user config Partial<ContainerConfig>
PresetRegistry Registry for container presets Class
registry.register(preset, options?) Register preset void
registry.registerMany(presets, options?) Register multiple presets void
registry.get(name) Get preset ContainerPreset
registry.has(name) Check if preset exists boolean
registry.list() List all preset names string[]
registry.remove(name) Remove preset boolean
registry.clear() Remove all presets void

Config & Validation

Method Description Return Type
buildContainerConfig(config) ContainerConfig → Docker config BuildContainerConfigResult
diffConfigs(oldConfig, newConfig) Calculate config diff ConfigDiff[]
validateResourceLimits(config) Validate resource config ConfigWarning[]
validateSecurityConfig(config) Validate security config ConfigWarning[]
validateProductionConfig(resources, security) Production validation ConfigWarning[]

Resilience

Method / Class Description Return Type
retry(fn, options) Execute function with retry Promise<T>
CircuitBreaker Circuit breaker pattern Class
withTimeout(promise, ms, message?) Timeout wrapper Promise<T>
DaemonMonitor Docker daemon monitoring Class
ShutdownManager Graceful shutdown manager Class
ResilientStream Stream with auto-reconnect Class

Full API documentation: docs/api.md


Configuration Reference

ContainerConfig

interface ContainerConfig {
  // === Base ===
  image: string;                     // Docker image (required)
  name?: string;                     // Container name
  tag?: string;                      // Image tag (default: "latest")
  cmd?: string[];                    // CMD override
  entrypoint?: string[];             // Entrypoint override
  env?: Record<string, string>;      // Environment variables
  labels?: Record<string, string>;   // Container labels
  workingDir?: string;               // Working directory

  // === Network ===
  portMappings?: PortMappingInput[]; // Port mappings (string/number/object)
  networks?: Record<string, {        // Custom networks
    aliases?: string[];
    ipv4Address?: string;
  }>;
  hostname?: string;                 // Container hostname
  domainName?: string;               // Domain name
  dns?: string[];                    // DNS servers

  // === Storage ===
  mounts?: MountInput[];             // Mounts (string/object)
  tmpfs?: Record<string, string>;    // Tmpfs mounts

  // === Resources ===
  resources?: ResourceConfig;        // CPU, memory, I/O limits

  // === Security ===
  security?: SecurityConfig;         // Detailed security config
  securityProfile?:                  // Security preset
    | "hardened"
    | "standard"
    | "permissive";

  // === Interactive / TTY ===
  interactive?: boolean;             // Enable OpenStdin + AttachStdin (default: false)
  tty?: boolean;                     // Enable pseudo-terminal (default: false)
  preset?: string;                   // Name of a registered preset

  // === Lifecycle ===
  restartPolicy?:                    // Restart policy
    | "no"
    | "always"
    | "unless-stopped"               // (default)
    | "on-failure";
  stopTimeout?: number;              // Graceful stop timeout in seconds (default: 10)
  healthCheck?: HealthCheckConfig;   // Health check configuration

  // === Meta ===
  production?: boolean;              // Enable strict warnings
  suppressWarnings?: string[];       // Suppress specific warnings
}

Defaults

Field Default Description
tag "latest" Image tag
restartPolicy "unless-stopped" Restart behavior
stopTimeout 10 Seconds until force-kill
securityProfile "standard" Security preset

Security Presets

Preset User ReadOnly FS Capabilities No New Privileges Seccomp
hardened 1000:1000 true Drop ALL true default
standard — — Docker default true default
permissive — — Docker default — —

Orchestrator Options

interface OrchestratorOptions {
  defaultNetwork?: string;           // Default network for all containers
  defaultSecurityProfile?:           // Default security preset
    | "hardened" | "standard" | "permissive";
  defaultLabels?: Record<string, string>; // Labels applied to all containers
  retryPolicy?: Partial<RetryPolicies>;   // Retry configuration
  circuitBreaker?:                   // Circuit breaker (false = disabled)
    | Partial<CircuitBreakerOptions>
    | false;
  timeouts?: Partial<TimeoutConfig>; // Timeout configuration
  logger?: Logger;                   // Custom logger
  daemonMonitor?:                    // Daemon monitoring
    | boolean
    | Partial<DaemonMonitorOptions>;
}

Error Handling Reference

Error Classes

All errors extend DockerOrchestratorError and include code, cause, context, and timestamp.

Class Code Description
Connection
ConnectionError CONNECTION_ERROR Failed to connect to Docker socket
DockerDaemonNotRunningError DOCKER_DAEMON_NOT_RUNNING Docker daemon not running
DockerApiVersionError DOCKER_API_VERSION_ERROR Incompatible API version
Container
ContainerNotFoundError CONTAINER_NOT_FOUND Container does not exist
ContainerNotRunningError CONTAINER_NOT_RUNNING Container is stopped
ContainerAlreadyRunningError CONTAINER_ALREADY_RUNNING Container is already running
ContainerAlreadyStoppedError CONTAINER_ALREADY_STOPPED Container is already stopped
ContainerAlreadyExistsError CONTAINER_ALREADY_EXISTS Name already taken
Image
ImageNotFoundError IMAGE_NOT_FOUND Image not found
ImagePullError IMAGE_PULL_ERROR Image download failed
Resource
PortAlreadyInUseError PORT_ALREADY_IN_USE Port in use
InsufficientResourcesError INSUFFICIENT_RESOURCES Not enough resources
OOMKilledError OOM_KILLED Out-of-memory kill
VolumeInUseError VOLUME_IN_USE Volume still in use
Operation
CommandFailedError COMMAND_FAILED Exec command failed
CommandTimeoutError COMMAND_TIMEOUT Command timed out
HealthCheckTimeoutError HEALTH_CHECK_TIMEOUT Health check timed out
DeploymentFailedError DEPLOYMENT_FAILED Deployment failed
RecreationFailedError RECREATION_FAILED Container recreation failed
CriticalRecreationError CRITICAL_RECREATION_ERROR Recreation + rollback failed
UpdateFailedError UPDATE_FAILED Update failed
BatchOperationError BATCH_OPERATION_ERROR Batch partially failed
TimeoutError TIMEOUT General timeout
CircuitOpenError CIRCUIT_OPEN Circuit breaker is open
Config
ValidationError VALIDATION_ERROR Config validation failed
InvalidResourceConfigError INVALID_RESOURCE_CONFIG Invalid resource config
InvalidSecurityConfigError INVALID_SECURITY_CONFIG Invalid security config
InvalidMountError INVALID_MOUNT Invalid mount specification
InvalidSubnetError INVALID_SUBNET IP not in subnet
Network
NetworkNotFoundError NETWORK_NOT_FOUND Network not found
NetworkAlreadyExistsError NETWORK_ALREADY_EXISTS Network already exists
ContainerStillConnectedError CONTAINER_STILL_CONNECTED Container still in network
Volume
VolumeNotFoundError VOLUME_NOT_FOUND Volume not found
VolumeAlreadyExistsError VOLUME_ALREADY_EXISTS Volume already exists
Attach / Console
StdinNotAvailableError STDIN_NOT_AVAILABLE Container does not have OpenStdin enabled
ConsoleDisconnectedError CONSOLE_DISCONNECTED Console is not connected
ConsoleCommandTimeoutError CONSOLE_COMMAND_TIMEOUT sendAndWait timeout exceeded
GracefulStopTimeoutError GRACEFUL_STOP_TIMEOUT Graceful stop timeout exceeded
Preset
PresetNotFoundError PRESET_NOT_FOUND Preset not found in registry
PresetAlreadyExistsError PRESET_ALREADY_EXISTS Preset already exists (without overwrite)
PresetValidationError PRESET_VALIDATION_ERROR Preset validation failed
ReadyCheckTimeoutError READY_CHECK_TIMEOUT Ready check timeout exceeded
Other
FileNotFoundError FILE_NOT_FOUND File not found
PermissionError PERMISSION_DENIED Access denied
SeccompProfileNotFoundError SECCOMP_PROFILE_NOT_FOUND Seccomp profile missing
DependencyResolutionError DEPENDENCY_RESOLUTION_ERROR Circular dependency
DockerInternalError DOCKER_INTERNAL_ERROR Internal Docker error

Transient vs. Permanent Errors

Transient errors (automatic retry possible):

  • CONNECTION_ERROR, DOCKER_DAEMON_NOT_RUNNING, TIMEOUT, COMMAND_TIMEOUT, DOCKER_INTERNAL_ERROR
  • Network errors: ECONNREFUSED, ECONNRESET, ETIMEDOUT, socket hang up

Permanent errors (no retry useful):

  • CONTAINER_NOT_FOUND, IMAGE_NOT_FOUND, VALIDATION_ERROR, PORT_ALREADY_IN_USE, etc.

Check with isTransientError(err):

import { isTransientError } from "@pruefertit/docker-orchestrator";

try {
  await someOperation();
} catch (err) {
  if (isTransientError(err)) {
    // Retry logic
  } else {
    // Permanent error — do not retry
    throw err;
  }
}

Retry Behavior per Operation

Operation Max Retries Initial Delay Description
imagePull 3 2000 ms Image download
containerStart 2 1000 ms Container start
healthCheck 0 — No retry (own logic)
exec 0 — No retry
dockerPing 5 500 ms Daemon connectivity

Troubleshooting

Docker socket permission denied

Error: connect EACCES /var/run/docker.sock

Solution:

# Add current user to the docker group
sudo usermod -aG docker $USER
# Re-login or:
newgrp docker

# Alternative: adjust socket permissions (less secure)
sudo chmod 666 /var/run/docker.sock

Cannot connect to Docker daemon

Error: DOCKER_DAEMON_NOT_RUNNING

Solution:

# Start Docker service
sudo systemctl start docker

# Check status
sudo systemctl status docker

# Check socket path
ls -la /var/run/docker.sock

# Use a custom socket path
const { docker } = await createClient({ socketPath: "/custom/path/docker.sock" });

Port already in use

Error: PORT_ALREADY_IN_USE - Port 8080 is already in use

Solution:

# Find process on port
lsof -i :8080
# or
ss -tlnp | grep 8080

# Kill the process or use a different port
// Programmatically: check port availability
import { checkPortAvailable } from "@pruefertit/docker-orchestrator";
const free = await checkPortAvailable(8080);

Image not found

Error: IMAGE_NOT_FOUND - Image not found: myapp:v1.0

Solution:

  • Check image name and tag: docker pull myapp:v1.0
  • Private registry: login required (docker login)
  • Check network connectivity to the registry

OOM Killed

Error: OOM_KILLED - Container was killed by OOM killer

Solution:

// Increase memory limit
await orch.deploy({
  image: "myapp",
  resources: {
    memory: {
      limit: "1g",           // Increase from 512m to 1g
      reservation: "512m",
    },
  },
});

// Monitor memory usage
const metrics = await getMetrics(docker, containerId);
console.log(`RAM: ${metrics.memory.percent}%`);

Health check timeout

Error: HEALTH_CHECK_TIMEOUT - Health check timed out

Solution:

await orch.deploy({
  image: "myapp",
  healthCheck: {
    type: "http",
    httpGet: { path: "/health", port: 8080 },
    interval: 15,        // Longer interval
    timeout: 10,         // More time per check
    retries: 10,         // More attempts
    startPeriod: 30,     // Longer warm-up period
  },
});

Also check:

  • Is the health endpoint reachable? (curl http://localhost:PORT/health)
  • Is the application starting correctly inside the container? (check logs)
  • Does the port in the health check match the container port?

Permission denied in container

Error: EACCES: permission denied

Solution:

// Set user/UID explicitly
await orch.deploy({
  image: "myapp",
  security: {
    user: "1000:1000",    // UID:GID matching the files
  },
});
# Fix volume permissions on the host
sudo chown -R 1000:1000 /host/data

# Or set USER instruction in your Dockerfile

License

MIT License — see LICENSE for details.

Links

Maintained by me.

About

docker-orchestrator @ npm

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages