mrkeyoor.com_
Sat 08 Aug 18:49 UTC
npmInfraupdated 08 Aug 2026

dockerode

A Node.js client for the Docker Engine HTTP API. It models the API as entities rather than loose functions: docker.getContainer(id) gives you a Container with start, stop, exec, logs and stats, docker.getImage(name) gives you an Image, and the same holds for networks, volumes, services, tasks, secrets and configs. Every method takes the options Docker documents and returns what Docker returns, unmodified. Streams from attach, logs, pull and build are handed to you intact instead of being buffered, and helper functions exist for the two things that always trip people up: following progress and demultiplexing a combined stdout and stderr stream.

Verdict

The default Node client for the Docker Engine API, and it has stayed that way by being a thin, complete and honest pass-through rather than an abstraction. Accept that you will read Docker's API reference alongside it and that TypeScript support is a third-party package running behind the releases.

API stability4/5The entity model and method names have been constant for years, and the 5.0.0 major in April 2026 was a dependency cleanup plus a raised Node floor rather than an API break. The instability you feel comes from Docker itself, since options and responses are passed through untouched, which is exactly why pinning the version option matters.
Docs3/5The README is a good tour: connection options, the callback and promise forms, docker.run, hijacked exec streams, and a complete method list per entity. Every one of those method entries is a link to the Docker Engine API rather than a description, so there is no parameter documentation here and no worked examples beyond the README and the examples directory.
Maintenance4/5The repository was pushed on 2026-08-05, 5.0.1 shipped on 2026-06-24 and 5.0.0 on 2026-04-23, with only 24 open issues and pull requests against 102 published versions. Much of the recent changelog is dependency bumps, which for a client library that tracks an external API is a reasonable steady state rather than a warning sign.
Ecosystem4/5Roughly 6,362,282 weekly downloads and 4,936 stars, and it is what testcontainers-node and a long tail of CI and orchestration tools build on. The surrounding pieces are its own siblings, docker-modem for the network stack and dockerode-compose, so the ecosystem is deep rather than wide. Typings living in DefinitelyTyped is the notable gap.

Use it if

  • You are automating Docker from Node and want direct API access rather than shelling out to the docker binary and parsing its output
  • You need the raw streams from logs, attach, exec or stats, since dockerode passes them through and does not buffer them for you
  • You are building test infrastructure or a CI runner that starts throwaway containers, which is what docker.run plus HostConfig.AutoRemove covers in one call
  • You need parts of the API that the CLI exposes awkwardly or not at all, including Swarm services, tasks, nodes, secrets and configs
Skip it if

Setup reality

npm install dockerode gets you going against /var/run/docker.sock with no configuration, which is also where the first real problem starts: whatever runs your code needs permission on that socket, and mounting it into a container hands that container root-equivalent access to the host. For remote daemons you pass host, port and the ca, cert and key buffers yourself; there is no DOCKER_HOST parsing beyond the defaults, and protocol is inferred unless you set it. Pin the API version explicitly with the version option once your daemon is newer than 1.13, otherwise a daemon upgrade can change response shapes underneath you. The streaming methods are where the design shows: buildImage and pull return a stream that has not finished when the promise resolves, so you must run it through modem.followProgress to know the work completed, and a build that fails still emits a stream that ends normally with the error inside the JSON events. When Tty is false, stdout and stderr arrive multiplexed in a single framed stream and printing it directly produces binary noise; modem.demuxStream is the fix. buildImage needs either a tar archive or an explicit src file list, and files not listed simply are not there during the build. Errors from the daemon come back as objects with statusCode and a json body rather than as typed errors, so error handling means checking numbers. TypeScript users need @types/dockerode separately and should expect gaps against the current release.

Patterns

Create a clientconnect-to-daemon

const Docker = require('dockerode');

const local = new Docker({ socketPath: '/var/run/docker.sock' });

const remote = new Docker({
  host: '10.0.0.5',
  port: 2376,
  ca: fs.readFileSync('ca.pem'),
  cert: fs.readFileSync('cert.pem'),
  key: fs.readFileSync('key.pem'),
  version: 'v1.44',
});

Pin version once your daemon is newer than 1.13. Without it the client uses the daemon default, so a host upgrade can change response shapes under working code.

Run a command and get the exit coderun-throwaway-container

const [result, container] = await docker.run(
  'alpine:3.20',
  ['sh', '-c', 'echo hello'],
  process.stdout,
  { HostConfig: { AutoRemove: true } }
);
console.log(result.StatusCode);

With AutoRemove the container is gone by the time the promise resolves, so do not call container.remove() afterwards. Without a callback, run resolves to a [result, container] pair.

Create, start, stop and remove explicitlycreate-and-start-container

const container = await docker.createContainer({
  Image: 'redis:7',
  name: 'test-redis',
  HostConfig: { PortBindings: { '6379/tcp': [{ HostPort: '0' }] } },
});

await container.start();
const info = await container.inspect();
console.log(info.NetworkSettings.Ports['6379/tcp'][0].HostPort);

await container.stop();
await container.remove();

HostPort '0' asks Docker for a free port, and inspect after start is the only way to learn which one you got.

Separate stdout from stderrdemux-output-streams

const stream = await container.attach({
  stream: true, stdout: true, stderr: true,
});
container.modem.demuxStream(stream, process.stdout, process.stderr);

Needed whenever Tty is false, because Docker frames both streams into one connection with 8-byte headers. Piping that stream directly prints control bytes.

Wait for an image build to finishfollow-build-progress

const stream = await docker.buildImage(
  { context: __dirname, src: ['Dockerfile', 'package.json', 'src'] },
  { t: 'myapp:latest' }
);

const output = await new Promise((resolve, reject) => {
  docker.modem.followProgress(stream, (err, res) => (err ? reject(err) : resolve(res)));
});

Every file the Dockerfile touches must appear in src or it will not exist in the build context. followProgress is the only way to know the build ended, and a failed build resolves with the error inside the events.

Pull an image and wait for itpull-image

const stream = await docker.pull('postgres:16');
await new Promise((resolve, reject) => {
  docker.modem.followProgress(stream, (err) => (err ? reject(err) : resolve()));
});

The promise from pull resolves as soon as the stream exists, not when the layers are down. Starting a container before followProgress finishes fails with 'no such image'.

Run a command inside a running containerexec-in-container

const exec = await container.exec({
  Cmd: ['sh', '-c', 'ls /app'],
  AttachStdout: true,
  AttachStderr: true,
});
const stream = await exec.start({ hijack: true, stdin: false });
container.modem.demuxStream(stream, process.stdout, process.stderr);

const detail = await exec.inspect();
console.log(detail.ExitCode);

exec.start does not report the exit status. Call exec.inspect() after the stream ends and read ExitCode, otherwise a failing command looks successful.

Follow container logsstream-logs

const logs = await container.logs({
  follow: true, stdout: true, stderr: true, tail: 100,
});
logs.on('data', (chunk) => process.stdout.write(chunk.toString()));

With follow: true you get a stream; without it you get a Buffer. Same multiplexing rule applies, so demux unless the container was created with Tty: true.

Read resource usage oncecollect-stats

const stats = await container.stats({ stream: false });
const cpuDelta = stats.cpu_stats.cpu_usage.total_usage
  - stats.precpu_stats.cpu_usage.total_usage;
console.log(stats.memory_stats.usage, cpuDelta);

stream: false returns a single sample. CPU percentage is not provided; you compute it from the delta against precpu_stats yourself, as the CLI does.

List containers with filterslist-and-filter

const containers = await docker.listContainers({
  all: true,
  filters: JSON.stringify({ label: ['app=web'], status: ['exited'] }),
});

filters must be a JSON string, not an object, because it is passed through as a query parameter. Passing an object gives you an unfiltered list with no error.

Authenticate against a private registrypull-private-image

const authconfig = {
  username: process.env.REGISTRY_USER,
  password: process.env.REGISTRY_TOKEN,
  serveraddress: 'https://index.docker.io/v1',
};

const stream = await docker.pull('acme/private:1.2.3', { authconfig });

docker-modem base64 encodes this for you, so do not pre-encode. Auth failures surface as a stream event rather than a rejected promise.

Read the status code from a failurehandle-daemon-errors

try {
  await docker.getContainer('missing').inspect();
} catch (err) {
  if (err.statusCode === 404) {
    console.log('not there');
  } else {
    throw err;
  }
}

Errors carry statusCode and json from the daemon rather than being typed classes, so branching on the number is the intended pattern. 409 means a conflicting state such as already started.

Alternatives

PackageRegistryPick it when
testcontainersnpmThe goal is disposable containers for integration tests and you want wait strategies, port allocation and cleanup handled
docker-composenpmYou just need to drive existing compose files from Node and do not want to model the API at all
execanpmShelling out to the docker CLI is genuinely enough, and you would rather depend on a binary you already trust than on an API client