mrkeyoor.com_
Sat 08 Aug 19:52 UTC
npmTestingupdated 08 Aug 2026

docker-compose

docker-compose is a Node.js wrapper around the Docker Compose command line. It starts a child process for commands such as up, down, build, exec, logs, and ps, then returns stdout, stderr, and the exit code in a promise. Version 1.4.2 defaults to the modern docker compose plugin, can still target the old standalone docker-compose binary, includes TypeScript types, and is mainly useful for bringing up integration-test dependencies from Node scripts.

Verdict

A practical thin wrapper when a Node test runner already owns a Compose file and only needs predictable command calls. Skip it for serious test orchestration or interactive processes, where Testcontainers or direct process control makes lifecycle and failure behavior clearer.

API stability4/5The package keeps a simple function-per-command API and returns the same out, err, and exitCode shape across commands. Version 1.0 changed the default from the retired docker-compose v1 executable to the docker compose v2 plugin, while preserving standalone mode as an option. The main instability comes from forwarding commandOptions to an external CLI whose flags and output formats can change independently of this wrapper.
Docs4/5The live VitePress site has separate pages for each supported command, common option tables, return types, progress callbacks, typed Compose objects, and parsed ps examples. The README prominently explains the v2 default and the 500 ms workaround. Some documentation is less precise than the source, including calling cwd required even though the TypeScript interface marks it optional, so source types still matter for edge cases.
Maintenance4/5The repository was pushed on 2026-08-05 and package 1.4.2 includes recent TypeScript, Vitest, Dockerode, and Compose schema work. GitHub reports 21 open issues and pull requests, and CI plus documentation deployment workflows are present. The README still contains old Node 6 and Node 8 testing language and describes the project as getting off the ground, which shows that not every explanatory section receives the same maintenance as the implementation.
Ecosystem4/5The npm package recorded 5,513,139 downloads in the last complete week despite a modest 222 GitHub stars, which strongly suggests widespread transitive or build-tool use. It fits ordinary Compose files and exposes most common CLI commands, but it has little plugin ecosystem of its own and depends completely on Docker, the Compose plugin, daemon permissions, registries, and host credential configuration.

Use it if

  • Your test runner or Node setup script needs to start and tear down an existing Compose project programmatically
  • You want typed wrappers for common Compose commands plus parsed results for ps, config, images, ports, version, and stats
  • You need to pass a Compose YAML string or typed ComposeSpecification object without first writing a temporary file
  • You support both the docker compose v2 plugin and an explicitly configured standalone docker-compose executable
Skip it if

Setup reality

npm install --save-dev docker-compose only installs the wrapper. The machine still needs a working Docker daemon and the docker compose v2 plugin on PATH, and the account running Node must have permission to talk to that daemon. Every call needs the correct cwd or config path; an incorrect directory often looks like a Compose configuration failure rather than a JavaScript error. The wrapper defaults to spawning docker with compose as its first argument. Old standalone installations require executable: { standalone: true }, optionally with an explicit executablePath. When you provide env, the source passes that object directly to child_process.spawn instead of merging it with process.env, so include process.env if the child still needs PATH, DOCKER_HOST, or credential-helper settings. run and exec force -T for non-interactive operation and split a string command on whitespace; use a string array for arguments containing spaces. up methods add -d unless commandOptions request an attached or abort-on-exit mode. Nonzero exits reject with a result-shaped object containing out, err, and exitCode, not necessarily an Error with a useful message. stop and down also wait a hard-coded 500 ms after the process exit because of a documented Compose v2 timing problem, which is still not a readiness or removal guarantee. Use try/finally around test suites so down runs after failures, and choose carefully before passing --volumes because teardown can remove persistent test data.

Patterns

Start a Compose project in detached modestart-all-services

import path from 'node:path';
import { upAll } from 'docker-compose';

await upAll({
  cwd: path.resolve('test/stack'),
  log: true,
});

upAll adds -d by default. Docker, the Compose plugin, the daemon, and any required registry credentials must already work for the current user.

Start only selected servicesstart-selected-services

import { upMany } from 'docker-compose';

await upMany(['postgres', 'redis'], {
  cwd: process.cwd(),
  commandOptions: ['--build'],
});

Dependencies declared by the Compose file may still start. Add --no-deps to commandOptions only when the selected services truly stand alone.

Always tear down after integration teststear-down-in-tests

import { down, upAll } from 'docker-compose';

await upAll({ cwd: stackDir });
try {
  await runIntegrationSuite();
} finally {
  await down({ cwd: stackDir, commandOptions: ['--remove-orphans'] });
}

The library applies a fixed 500 ms post-exit delay to Compose v2 stop and down calls, but that is not proof that every resource has disappeared.

Remove containers and named volumesremove-test-volumes

await compose.down({
  cwd: stackDir,
  commandOptions: ['--volumes', ['--timeout', '20']],
});

--volumes deletes named volumes declared by the project. Use it for disposable test data, not a developer stack with state worth keeping.

Run a disposable service commandrun-one-off-command

const result = await compose.run(
  'app',
  ['npm', 'test', '--', '--runInBand'],
  { cwd: stackDir, commandOptions: ['--rm', '--no-deps'] }
);
console.log(result.out);

Pass an argument array when quoting or spaces matter. A string is split on whitespace, and run always adds -T for non-interactive execution.

Execute a command in a running serviceexec-in-running-service

const result = await compose.exec(
  'app',
  ['node', 'scripts/health check.js'],
  { cwd: stackDir }
);
console.log(result.exitCode, result.out);

exec targets an already running Compose service and adds -T. An argument array preserves the filename containing a space.

Read machine-friendly service statesinspect-service-state

const result = await compose.ps({
  cwd: stackDir,
  commandOptions: [['--format', 'json']],
});

for (const service of result.data.services) {
  console.log(service.name, service.state, service.ports);
}

JSON format yields defined state names such as running or exited. The default table parser returns human text such as 'Up 2 hours'.

Validate and list configured servicesvalidate-compose-config

const validated = await compose.config({ cwd: stackDir });
const listed = await compose.configServices({ cwd: stackDir });

console.log(validated.data.config);
console.log(listed.data.services);

config parses Compose's YAML output, while configServices splits the command output into names. Both reject when Compose exits nonzero.

Start from a typed Compose objectprovide-compose-object

import { type ComposeSpecification, upAll } from 'docker-compose';

const compose: ComposeSpecification = {
  services: {
    cache: { image: 'redis:7-alpine', ports: ['0:6379'] },
  },
};

await upAll({ compose });

The object is converted to YAML and written to docker compose over stdin. Pin image versions instead of using moving tags in repeatable tests.

Merge multiple Compose filesuse-multiple-config-files

await compose.upAll({
  cwd: stackDir,
  config: ['compose.yml', 'compose.test.yml'],
});

Each path becomes a separate -f argument in the supplied order and is interpreted relative to cwd. configAsString takes precedence if both are supplied.

Receive output while a command runsstream-command-output

await compose.buildAll({
  cwd: stackDir,
  parallel: true,
  callback: (chunk, source) => {
    process[source === 'stderr' ? 'stderr' : 'stdout'].write(chunk);
  },
});

The callback receives chunks immediately, but the library still accumulates complete stdout and stderr in memory for the final result.

Add variables without losing PATHpreserve-child-environment

await compose.pullAll({
  cwd: stackDir,
  env: {
    ...process.env,
    COMPOSE_PROJECT_NAME: 'mrkeyoor-tests',
  },
});

The env option replaces the child's full environment. Spread process.env or Docker, PATH, DOCKER_HOST, and credential settings may disappear.

Alternatives

PackageRegistryPick it when
testcontainersnpmUse it for per-test containers, readiness strategies, mapped ports, and automatic lifecycle cleanup
dockerodenpmUse it when you need direct Docker Engine API control rather than invoking the Compose CLI
execanpmUse it when a general process runner plus a few explicit docker compose commands is clearer than a command-specific wrapper