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
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
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
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!