Dockerizing Development Environment in Mac: Performance Enhancement

Recently, I moved from Ubuntu to Mac for development environment of a project. I ran dockerized development environment based on docker-compose, and I found it to be painfully slow. You can find numerous posts on discussion forums like this, this, this etc. which prove the slowness of Docker in Mac.

In this post, I will mention some techniques that you can use to improve the performance. While you may not be able to make it fully as performant as on Linux environments, these steps will help massively.

1. Using docker-sync

Docker-sync is an alternative tool to the native volumes sharing of Docker for Mac. It provides different sync strategies each with its own pros and cons. You have to choose the one that is suitable for you.

Here’s an example configuration using rsyncstrategy:

Boilerplate for docker-sync using rsync strategy

After that, run the following commands in the root of your project:

You can browse the configuration guide for docker-sync.yml here.

You can also check out the multiple boilerplate demonstrating different strategies available in EugenMayer/docker-sync-boilerplate repository.

2. Using cached and delegated flag in docker volumes

In many cases, when the docker volume’s sync is very slow, it is because of the perfect consistency by default between host and the container and in many cases, the perfect consistency is unnecessary.

In many cases, there is no need for writes performed in a container to be immediately reflected on the host and in many cases, there is no need for writes performed in the host to be immediately reflected in a container.

According to the official documentation of Docker for Mac,

Docker 17.04 CE Edge adds support for two new flags to the docker run -v, --volume option, cached and delegated, that can significantly improve the performance of mounted volume access on Docker for Mac.

There are 3 different flags that you can use in the --volume option:

  1. consistent: perfect consistency (host and container have an identical view of the mount at all times)
  2. cached: the host’s view is authoritative (permit delays before updates on the host appear in the container)
  3. delegated: the container’s view is authoritative (permit delays before updates on the container appear in the host)

Here’s an example using cached flag as a suffix to -v option:

docker run -it -v /Users/ujjwal/project:/app:cached alpine command

3. Only syncing the required files and folders

In many cases, we sync auto-generated files, platform-specific files generated during compile time or program running time etc.

For example, if you are using a node project, I can blindly bet that you have node_modules folders in megabytes. In many cases, you can install the npm dependencies in container and you don’t really have to sync that folder between host and the container.

Here’s a way to prohibit the syncing of node_modules folder.

volumes:
- './myApp:/opt/app'
- /opt/app/node_modules/

Techniques like this should be used to prohibit the unnecessary syncing of files and folders and to improve the performance massively. Don’t sync what is not required.

4. Optimizing development environment tooling

This may look very obvious. But in a lot of cases, you ignore the performance of development environment tooling. Spending a little bit of time may have massive improvements in performance, even more than you expect.

This actually turned out to be the most significant performance improving step for me. On my Mac, the CPU percentage of docker hyperkit process as shown on Activity Monitor reduced from 150% to 7%. I will explain the specific optimizations I did in that project.

It was a node project, node.js on the backend and React.js on the frontend.

I had to set chokidar’s usePolling option as true. Otherwise, file changes were not detected in docker volumes. The reason for that is there’s an issue in docker, so file changes are not detected in mounted volumes in Mac or Windows because filesystem events are not propagated from host to container.

By default, the polling interval is 100. I set that to 250, and it was a fair enough trade I was willing to make for performance.

Another thing that I did was to ignore watching node_modules folder. In a non-docker environment, watching node_modules was not a big performance problem. But, with chokidar’s polling in a dockerized environment, this was the deal breaker for me which made the most significant improvement in my project.

Conclusion

Docker for Mac has a lot of problems performance wise. But, there are ways to overcome the issue. Sometimes, it’s an issue with the tools itself but there may be ways to minimize the problems. Changing the tools and moving to better and new tools is not always the solution.

In this case, we dig deeper when the development environment was frustratingly slow and applied some tricks to create a much more performant and resource efficient development environment in docker.