# Dockerode: Docker's Remote API from Node.js, Without the Wrapper Magic

> Dockerode is a Node.js client for Docker's Remote API that passes streams through untouched and models containers, images and execs as entities. It suits engineers who already know the Docker API and want it in JavaScript, not a new abstraction over it.

**apocas/dockerode** — Docker + Node = Dockerode (Node.js module for Docker's Remote API)

- Repository: https://github.com/apocas/dockerode
- Stars: 4,947 · Forks: 488
- Language: JavaScript
- License: Apache-2.0
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/apocas-dockerode

## What Dockerode Is For, and Who Should Care

Dockerode is a Node.js module for Docker's Remote API. The README opens by saying it is "Not another Node.js Docker Remote API module", and the objectives list explains what that means in practice: streams are not broken, containers and images and execs are entities rather than static methods, and both callback and promise interfaces exist.

The intended user is someone who already knows the Docker API. The README states that input options are passed directly to Docker, return values are unchanged from Docker, and official Docker documentation applies to both. That is the whole design stance. Dockerode is a transport and an object model, not a re-description of Docker's semantics.

That makes it a poor fit for someone who wants a declarative container definition in JavaScript. It is a good fit for a CI runner, a deployment script, a log collector or an internal dashboard that needs to list, start, stop, attach to and inspect containers on a host it can reach. If you find yourself reading the Docker API reference anyway, dockerode is the shortest path to calling it from Node.

## How Dockerode Maps the Docker Remote API onto JavaScript Objects

The architecture has two layers. Dockerode itself defines entities and convenience methods; the network stack lives in a separate package, docker-modem, which the README lists under Ecosystem as "Docker's API network stack". The dependency list in package.json confirms this: docker-modem is a runtime dependency, alongside tar-fs for build contexts, @balena/dockerignore, and gRPC packages.

The entity model is the part worth understanding before you write code. Calling docker.getContainer('71501a8ab0f8') does not query the API, per the README comment: it creates a container entity. The API call happens when you invoke a method on that entity, such as inspect, start or remove. The same pattern applies to images and execs. This matters because it means errors surface at the method call, not at the getter.

Streams are passed through rather than consumed. When a container runs with Tty false, the attach stream is multiplexed and dockerode can demultiplex it for you through container.modem.demuxStream, routing stdout and stderr to separate writable streams. When Tty is true there is no multiplexing to undo, so the README's example pipes the attach stream straight to process.stdout. The module also exposes HTTP connection hijacking, which the README describes for commands whose stdin and stdout must be closed independently. The comment in that example is explicit about why: without a socket upgrade there is no way to close the write side of a stream without closing the read side.

One more mechanism worth noting: container.defaultOptions lets you set options that are always applied for a given container and operation. The README example is container.defaultOptions.start.Binds = ["/tmp:/tmp:rw"]. This is a per-entity default, not a global one, so it does not leak across containers you create later.

## Installing Dockerode and Running Your First Container

Installation is a single npm command. The README gives it as npm install dockerode, and package.json requires Node >= 14.17.

```bash
npm install dockerode
```

The first real use is instantiating the client. The README shows several constructor forms. The default constructor targets the local socket, and a host and port form targets a remote daemon.

```js
var Docker = require('dockerode');
var docker = new Docker({socketPath: '/var/run/docker.sock'});
var docker1 = new Docker(); //defaults to above if env variables are not used
var docker2 = new Docker({host: 'http://192.168.1.10', port: 3000});
```

For TLS against a remote daemon, the README passes ca, cert and key as file contents, and notes that version is required when Docker >= v1.13. The README's example uses 'v1.25' with a pointer to Docker's API version history page, so treat that string as a placeholder for whatever your daemon reports rather than a recommendation.

Creating and starting a container follows the entity pattern. The README's promise example creates a container from the ubuntu image, starts it, resizes the TTY, stops it and removes it.

```js
docker.createContainer({
  Image: 'ubuntu',
  AttachStdin: false,
  AttachStdout: true,
  AttachStderr: true,
  Tty: true,
  Cmd: ['/bin/bash', '-c', 'tail -f /var/log/dmesg'],
  OpenStdin: false,
  StdinOnce: false
}).then(function(container) {
  return container.start();
}).catch(function(err) {
  console.log(err);
});
```

The object keys are Docker API fields, capitalized as Docker defines them (Image, Cmd, Tty). If you get a 400 from the daemon, the cause is usually a field name or value that Docker rejected, not a dockerode bug.

Building an image is the one place where the API shape surprises people. The README states that files involved in the build must be explicitly listed in the src array, because they are sent to a temporary environment. buildImage returns a promise of a Node stream, and to know when the build finished you follow progress through dockerode.modem.followProgress.

```js
let dockerode = new Dockerode();
let stream = await dockerode.buildImage(...);
await new Promise((resolve, reject) => {
  dockerode.modem.followProgress(stream, (err, res) => err ? reject(err) : resolve(res));
});
```

Without that followProgress step, awaiting the promise resolves when the stream is handed to you, not when the image exists. That is a real trap for anyone who assumes the await covers the build.

## Where Dockerode Gets in the Way

The build context rule is the sharpest limitation. Because the context is packed and sent to a temporary environment, any file a COPY instruction needs must appear in the src array. Miss one and the build fails at that instruction, even though the file sits right there on disk next to the Dockerfile. There is no fallback that reads from the working directory.

The API version is a second constraint. The README says version is required when Docker >= v1.13. Dockerode does not negotiate this for you in the examples shown; you supply the string. If your daemon is newer than the API version you pass, you are limited to that older surface, and newer endpoints will not be reachable. The README points at Docker's version history page rather than stating which versions v5.0.1 covers, so the mapping between the module version and the API version is something you check yourself.

Error handling is thin by design. Return values are unchanged from Docker, which means you get Docker's error shapes and status codes, not a normalized JavaScript error hierarchy. Code that needs to distinguish "container not found" from "daemon unreachable" has to inspect what Docker returned.

Finally, dockerode is a client. It does not manage a daemon, schedule containers, or reconcile desired state against actual state. Listing containers and stopping them one by one, as the README's example does, is the level of orchestration you get out of the box. Anything more is your code.

## Dockerode Compared with Shelling Out to the Docker CLI

The obvious alternative is child_process with the docker command, and the difference is not cosmetic. The CLI gives you a stable, human-oriented interface and handles context, credentials and output formatting. Dockerode gives you the API directly, which means you get structured return values instead of parsed text, and you get streams you can pipe into application code.

The stream handling is the clearest divergence. With the CLI you get a process whose stdout you read as bytes and whose exit code you check. With dockerode you get the attach stream and, for non-TTY containers, a demultiplexing helper that separates stdout from stderr for you. The README's hijack example goes further: it attaches stdin to an exec so a command like shasum can read a file from the host and finish when that stdin closes. Reproducing that with the CLI means relying on the shell's pipe semantics and giving up the ability to close one direction of the stream independently.

The trade-off runs the other way too. The CLI is one process boundary away from a shell you already trust, and it works identically in a terminal and in a script. Dockerode requires your Node process to have access to the socket or the TLS material, which is a different security posture. If all you need is docker ps in a script, the CLI is less machinery.

## Maintenance, Licensing and Upgrade Cost

The repository is not archived, and the last push was on 2026-09-13. The most recent release is v5.0.1, published on 2026-06-24, following v5.0.0 on 2026-04-23 and v4.0.10 on 2026-03-20. The 5.x line is recent enough that anyone on 4.x should read the release notes before upgrading rather than assuming a drop-in change.

Licensing is Apache-2.0, stated in both package.json and the LICENSE file at the repository root. That is a permissive licence with an explicit patent grant, which matters if you are embedding the client in a product rather than using it as an internal tool. It is not a copyleft licence, so it does not obligate you to publish your own source. This is a description of what the licence says, not legal advice; your counsel decides how it applies to your distribution.

The dependency surface is small but not zero: docker-modem, tar-fs, @balena/dockerignore, and gRPC packages for the protobuf-related paths. Each is a version range in package.json, so a lockfile is what actually pins your install. Upgrade cost is dominated by the Docker API version you target, not by dockerode's own API, which has stayed close to Docker's shape.

## Conclusion

Adopt dockerode when your service already speaks the Docker Remote API and you want that surface in Node.js without a second abstraction layer. Skip it if you want a declarative container spec, or if you need a maintained client for a Docker API version newer than what v5.0.1 targets. Verify the API version your daemon reports against the version you pass in the constructor, and check that socketPath or host/port matches how your daemon is actually reachable.

## FAQ

### What is dockerode?

It is a Node.js module for Docker's Remote API, published as dockerode on npm under Apache-2.0. The README describes it as passing streams through untouched and modelling containers, images and execs as entities rather than static methods.

### How do I install dockerode?

The README gives the install as npm install dockerode. package.json requires Node >= 14.17.

### Does dockerode support promises as well as callbacks?

Yes. The README lists callback and promise based interfaces among the project's objectives, and shows both forms in the usage examples. The promise library can be swapped through the Promise option in the constructor.

### How do I know when a dockerode buildImage call has finished?

buildImage returns a promise of a Node stream, so the promise resolving does not mean the build is done. The README shows following the build through dockerode.modem.followProgress on that stream.

## Sources

- [apocas/dockerode on GitHub](https://github.com/apocas/dockerode)
- [Issues](https://github.com/apocas/dockerode/issues)
- [License: Apache-2.0](https://github.com/apocas/dockerode/blob/master/LICENSE)
- [README](https://github.com/apocas/dockerode/blob/master/README.md)
- [Releases](https://github.com/apocas/dockerode/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/apocas-dockerode
