Jonathan Cremin

Improving Node.js Performance in Docker for Mac

Docker for Mac introduced a new hypervisor based on xhyve/bhyve with the goal of making development using Docker on macOS more seamless. It also introduced osxfs, a bridging file system for transparently mounting volumes from the host OS through to Docker in the Linux VM. Importantly, this file system supports translating from FSEvents to inotify and removes the need for polling files for changes.

Unfortunately there’s still a lot of CPU overhead and bugs, but in my unscientific testing reducing the number of files inside mounted volumes significantly mitigated both problems.

Docker and Node.js logos

At Udemy, we have a pretty hefty Django repository which also houses our frontend and it’s build tools. This means a package.json and node_modules directory.

Let’s get a breakdown of where most of our files are.

# Overall repo file count
❯ django git:(master) find . -type f | wc -l
# Django codebase
❯ django git:(master) find udemy -type f | wc -l
# Frontend including our code, bower and npm.
❯ django git:(master) find static -type f | wc -l
# Just npm packages
❯ django git:(master) find static/node_modules -type f | wc -l

Node’s module resolution stuff starts by checking node_modules in the current directory and traverses up the directory tree doing the same. To cut down on this overhead, we can tell Node exactly where we put them using NODE_PATH. You can use a colon separated list for this, but we don’t need anything more than /node_modules.

NODE_PATH only works for resolution by Node, so we can’t easily just tell it to install there. There is the --prefix argument of npm too, but this only applies to --global and we can’t use that with a package.json.

So we can’t npm install to a path other than node_modules alongside package.json, that means we have to copy package.json inside the container and off the osxfs mount and install there.

Here’s a simplified version of our Dockerfile. We use a prebuilt parent image with our dependencies already installed to speed up rebuild time during development.

FROM udemy/jessie-python-node:3.4-4.5-4
COPY static/package.json /package.json
RUN npm install
COPY requirements requirements
RUN pip3 install --no-cache-dir -r requirements/dev.txt
ENV NODE_PATH /node_modules

We can now run any node code from /app/static and it will use the packages from /node_modules. Let’s see what our codebase looks like now.

❯ django git:(master) find . -type f | wc -l

Much better! The remaining big offender for us is a bower_components directory (7,678). It’s possible for us to do the same with Bower, but it’s more invasive and we’re migrating those dependencies to npm anyway.

Hopefully Docker ships some performance and stability improvements to osxfs in the near future that negate the need for this. I think in the interim an .osxfsignore type of file would let us remove our hacks at least. Doing a two-way ignore is admittedly more complex than something like .dockerignore, but Bittorrent Sync seems to do a reasonable job of it.

I'm an engineering manager living in Dublin and working at Udemy. I've built stuff for Mac, iOS, and the web, like Hostr and Want to get in touch? Email or toot @kudos.