Docker on MacOS will always be slower than on Linux (well, unless Apple Silicon ends up panning out, which it looks like it might!), but it doesn't have to be as infuriatingly slow as it is by default. In fact, you can get it pretty close to native speed and it's not even all that difficult. It turns out that developing with Docker on MacOS can actually be a really enjoyable experience, not just a compromise you make in order to have a portable development environment.
The problem is that the default file sync strategy that Docker on MacOS uses is garbage. Sure, it's consistent and compatible, but it's just about the slowest possible option you could go with. As your Docker app runs, it reads and changes files across that sync, which means that every file access slows down your application a little bit more. Compounded, that means that even your average runs are bitterly slow.
Enter Mutagen. Mutagen does a lot of things and is a very interesting technology in general, but what we're primarily interested in is how it can perform lightning-quick syncs between your local computer and a Docker volume. How fast is it? Well, the title of this article isn't hyperbole, in fact, it's underselling things. My Jest test suite went from taking 73.5 seconds to run to only taking 22.9 seconds (on average).
So now that we know what the problem is and what the solution is, let's fix Docker on MacOS.
First, you have to install Mutagen, and as of the writing of this article, you need to install a beta release. We need version 0.12.0 or higher, since that's the version that starts supporting mutagen compose
(which we'll get to in a minute). There should be fairly low risk from this, though, since Mutagen's own website says:
Beta channel releases are fairly stable and can be thought of as something akin to release candidates.
To install Mutagen and start the sync daemon, first, make sure you have Homebrew installed, and then:
$ brew install mutagen-io/mutagen/mutagen-beta
$ mutagen daemon start
Next, you'll need to modify your docker-compose.yml
file to define the sync that Mutagen will spin up. Assume your current volumes
entry for a container looks like:
volumes:
- .:/code
You'll want to create a new section at the bottom of your file with an x-mutagen
configuration. mutagen compose
will read that and build your sync from the options you set there:
x-mutagen:
sync:
defaults:
ignore:
vcs: true
mode: "two-way-resolved"
mount-code:
alpha: "."
beta: "volume://mount-code"
alpha
is where you're syncing from, and beta
is the sync mount point that Mutagen will create. Also of note is the two-way-resolve
sync mode we're using. Mutagen has great documentation on how its synchronization modes work, but in short, we're using the mode that prioritizes host files. Since this is development and the files on the host system are the canonical ones anyway, this is safe and prevents conflicts that have to be manually resolved.
Finally, we have to update our volumes
entry to use the new mount point:
volumes:
- mount-code:/code
And that's it! Well, that's nearly it. docker-compose
doesn't know how to read an x-mutagen
entry, so instead we now run this file with the mutagen compose
command, which is essentially a pass-through to docker-compose
after having performed the various sync setup steps you've defined.
The first time you run this will be longer than before, as Mutagen will need to create the sync mount and perform the initial synchronization, but then it will run as a daemon and keep changes automatically in sync. I have found that occasionally it will run wild with the CPU for a couple dozen seconds as it updates its index (or something), but it hasn't significantly affected my development, especially not when compared against the incredible speed upgrades it provides.
Want to go really fast when starting your new SaaS business? Try Nodewood, a Node.js SaaS starter kit! Don't spend weeks or months writing your own user authentication, subscription payments system, team management, and more - just use Nodewood and get all that out of the box! Start writing your business logic today when you build with Nodewood!
Using Mutagen in Nodewood
Now that you have an idea how Mutagen works and how to set it up for your average project, I wanted to provide explicit steps for using it in your Nodewood projects.
There are two steps to using Mutagen in Nodewood: 1) Using an application-specific docker-compose.yml
file, and modifying your .nodewood.js
configuration file to use Mutagen. (Of course, you'll need to make sure you've already installed Mutagen according to the instructions above.)
1) Set up an app-specific docker-compose.yml file
Nodewood will use a file in the app
folder before it will use a file in the wood
folder, and this applies to your Docker files as well. Create a file in app/docker/docker-compose.yml
, and paste the following:
version: '3'
services:
postgres:
build:
context: .
dockerfile: ../../wood/docker/Dockerfile.postgres
networks:
- app-network
environment:
POSTGRES_PASSWORD: nodewood
POSTGRES_MULTIPLE_DATABASES: "nodewood,test"
expose:
- 5432
ports:
- '5432:5432'
ui:
build:
context: ../..
dockerfile: wood/docker/Dockerfile
env_file: ../../.env
volumes:
- nodewood-code:/nodewood
command: yarn dev-ui
ports:
- '8888:8888'
api:
build:
context: ../..
dockerfile: wood/docker/Dockerfile
networks:
- app-network
volumes:
- nodewood-code:/nodewood
ports:
- '3000:3000'
links:
- postgres
depends_on:
- postgres
command: yarn dev-api
nginx:
build:
context: .
dockerfile: ../../wood/docker/Dockerfile.nginx
networks:
- app-network
ports:
- '80:80'
- '443:443'
links:
- api:api
networks:
app-network:
driver: bridge
volumes:
nodewood-code:
x-mutagen:
sync:
defaults:
ignore:
vcs: true
mode: "two-way-resolved"
nodewood-code:
alpha: "../.."
beta: "volume://nodewood-code"
This will set up your Mutagen sync mount, and ensure that you're continuing to use the Dockerfiles for Nginx and Postgres that live in the wood/docker
folder.
2) Modify your Nodewood configuration file
In your project root, you'll find a file called .nodewood.js
. This file contains your Nodewood project information, including your keys that are used to authenticate with the Nodewood server when updating the library code. All you need to do is add the following two lines to the exports
object:
composeCommand: 'mutagen',
composeArgs: ['compose'],
This will instruct the Nodewood CLI to use mutagen compose
instead of docker-compose
when starting your project for development or tests.
NOTE: You will need to have v0.13.2 or later of the Nodewood CLI tool installed. You can update with yarn global add @nodewood/cli
.
That's it! Some simple copy and paste, and your Nodewood development experience should be dramatically improved!