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.
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.
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
- Docker or the Compose plugin is not already installed and reachable: the README states that Compose is still required, so this package does not provide an engine or daemon
- You need container lifecycle isolation, readiness checks, random ports, and automatic cleanup per test: Testcontainers supplies those testing abstractions while this wrapper only runs Compose commands
- You need shell-accurate command quoting: exec and run split string commands on whitespace, so quoted arguments and spaces are safer as explicit string arrays
- You need exact completion timing after stop or down: the README documents a fixed 500 ms delay because docker compose v2 can report an exit code before container work appears finished
- You want interactive exec sessions or attached terminals: exec and run always add -T, and the promise API buffers complete stdout and stderr even when a callback also receives chunks
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
| Package | Registry | Pick it when |
|---|---|---|
| testcontainers | npm | Use it for per-test containers, readiness strategies, mapped ports, and automatic lifecycle cleanup |
| dockerode | npm | Use it when you need direct Docker Engine API control rather than invoking the Compose CLI |
| execa | npm | Use it when a general process runner plus a few explicit docker compose commands is clearer than a command-specific wrapper |