Okay, if you are reading this, you probably already know that Docker (and not just Docker Desktop) on a MacOS can be awfully slow, especially for developers when the container is accessing files on the local drive via a volume mount. For the most part, this has not really been noticeable with the applications I develop against and so I have been happy to use the defaults. More recently though I have been working on a PHP application and due to its nature it required that "nearly" the whole development space needed to be mapped to a volume and the impact was very apparent even when using the gRPC FUSE option.

Starting point

Using a fairly standard delegated mapping like the one below, we found that the average time to open our home page locally was ~600ms (with the gRPC fuse setting)

version: '3.5'

services:
  app:
    volumes:
      - ./:/var/www/:delegated

Though not the simplest page it wasn't the most complex we have to deal with, can we do better?

Using NFS Mounts

Now a common approach to addressing this issue has been to use an NFS mount instead, setting it up is quite complicated but thankfully it is well documented and scripts are readily available for us to use.

version: '3.5'

services:
  app:
    volumes:
      - nfsmount:/var/www/

volumes:
  nfsmount:
    driver: local
    driver_opts:
      type: nfs
      o: addr=host.docker.internal,rw,nolock,hard,nointr,nfsvers=3
      device: ":${PWD}"

Switching to the above config for mounting the file system and running the same home page test reduced our average load time to ~290ms.  This is a pretty good improvement but can we do better?

Using Mutagen

Mutagen is a file synchronization utility that can be used with containers and more recently, with the beta version at least, it can be configured using docker-compose files.

Installing

Installing is quite easy, though it may take some time; there may be a faster approach but I just followed the hints from homebrew.

xcode-select --install
brew install mutagen-io/mutagen/mutagen-compose-beta
mutagen daemon start

Configuring

Since we are using the beta we can define the mounts in our docker-compose files.

version: '3.5'

services:
  app:
    volumes:
      - code:/var/www/

volumes:
  code:

x-mutagen:
  sync:
    defaults:
      ignore:
        vcs: true
        paths:
          - .DS_Store
          - .git
          - .idea
          - .vscode
      permissions:
        defaultFileMode: 0644
        defaultDirectoryMode: 0755

    code:
      alpha: "."
      beta: "volume://code"
      mode: "two-way-resolved"

Note: the permissions section was required as mutagen applies the default permissions of 0600 and 0700 for files and directories respectively however I require the permissions to be 0644 and 0755 so that I could launch the web application (nginx).

Finally, we need to use mutagen-compose instead of docker-compose on the command line (or in scripts). It takes a little longer to start due to the initial build of the docker volume; this can be tweaked by being more selective on what files need to be synchronized between the host and the docker volume.

Running the same test again, against the home page, we managed to reduce the average load time to ~160ms.

Summary

All of the above tests were done for a developer build, performance is much better for production builds which are hosted in the cloud.

methodload time (ms)
default~600
nfsmount~290
mutagen~160

It seems that mutagen is definitely a candidate to mitigate some of the performance issues encountered with Docker on the MacOS and should be considered alongside other utilities like docker-sync (requires ruby) and docker-bg-sync.

As always, I hope you find this useful and your feedback is appreciated.