from bind mount overhead to VirtioFS tuning
Solving Docker performance problems starts with understanding why the three platforms have fundamentally different architectures. On Linux, Docker runs natively; on macOS and WSL2, it runs through a virtualization layer, with measurable consequences for builds, bind mounts, and I/O-heavy applications.
Table of Contents
- 1. Why Docker performs differently across platforms
- 2. Linux: native performance as the baseline
- 3. macOS: virtualization and file system overhead
- 4. VirtioFS: the current state of macOS file system performance
- 5. WSL2: between Linux and Windows
- 6. Optimizing bind mounts
- 7. Resource limits and memory configuration
- 8. BuildKit cache and layer optimization for faster builds
- 9. Platform performance compared
- 10. Summary
- 11. FAQ
1. Why Docker performs differently across platforms
The fundamental difference in Docker performance between platforms is not rooted in Docker itself, but in the isolation layer beneath it. Docker containers are not virtual machines: they share the kernel of the host operating system. On Linux, that means containers run directly on the host kernel, with no virtualization layer in between. On macOS and Windows, there is no Linux kernel available, so Docker Desktop has to boot a Linux VM. Every system call from a container passes through this virtualization layer, adding measurable latency to I/O operations.
The consequences for Docker performance vary widely depending on the workload. CPU-intensive operations such as compiling code or running database queries are often only slightly slower on macOS than on Linux, because the virtualization layer handles processor operations very efficiently. The real difference shows up with file system operations: a PHP Composer install that writes thousands of small files, or a Webpack build with many dependencies, can take three times as long on macOS with bind mounts as it does on Linux. Understanding this root cause is the prerequisite for any targeted optimization.
2. Linux: native performance as the baseline
On Linux, Docker runs with almost no overhead. Containers share the host kernel, file system operations go through the native VFS layer, and network packets are routed directly over the Linux bridge. This is the reference platform against which Docker performance benchmarks are usually measured. Bind mounts on Linux are, in effect, just kernel mounts: the container process sees the exact same inode as the host, with no synchronization layer in between.
Even on Linux there is room to tune Docker performance. The choice of storage driver affects layer operations during image builds: overlay2 is the recommended driver and offers the best balance of performance and compatibility. devicemapper is outdated and should no longer be used. On Linux systems running many containers at once, tuning kernel parameters such as fs.inotify.max_user_watches and vm.max_map_count can matter, particularly for Elasticsearch or other memory-map-intensive services.
# Check current Docker storage driver and performance-relevant kernel params
docker info --format '{{.Driver}}' # Should be: overlay2
docker info --format '{{.MemTotal}}' # Total memory available to Docker
# Linux: check inotify limits (relevant for file watchers in development)
cat /proc/sys/fs/inotify/max_user_watches # Default: 8192, often too low
# Increase inotify limit for development environments
sudo sysctl -w fs.inotify.max_user_watches=524288
# Persist across reboots
echo 'fs.inotify.max_user_watches=524288' | sudo tee /etc/sysctl.d/99-docker-dev.conf
# Elasticsearch / OpenSearch: requires higher mmap limit
sudo sysctl -w vm.max_map_count=262144
# Check container I/O stats in real time
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.BlockIO}}"
3. macOS: virtualization and file system overhead
Docker Desktop on macOS uses Apple's Virtualization Framework (since macOS 12) or the older xhyve/HyperKit hypervisors to run a Linux VM. Every container runs inside that VM. The biggest Docker performance problem on macOS is the file system synchronization between the macOS file system (APFS) and the Linux file system inside the VM. When a bind mount maps a macOS folder into a container, changes have to be synchronized in both directions, across a virtualization boundary.
Older versions of Docker Desktop used gRPC FUSE for this synchronization, which was extremely slow for projects with lots of small files. A typical Magento or Symfony project with thousands of PHP files could be dramatically slower on the first build or the first dependency installation. These performance issues are inherent to the architecture: no amount of Docker tuning can eliminate the fundamental overhead of cross-VM file system synchronization. Optimizations can shrink the gap, but they cannot bring it down to Linux level.
4. VirtioFS: the current state of macOS file system performance
VirtioFS is Apple's implementation of the Virtio file system protocol. It has been available in Docker Desktop since macOS 12.3 and has been the default file system backend since version 4.6. Compared to gRPC FUSE, VirtioFS delivers significantly better Docker performance for bind mounts, because it is integrated more deeply into the macOS kernel and adds less overhead per file system operation. Real-world measurements show a two to three times improvement over gRPC FUSE for Composer installs and Webpack builds.
Even with VirtioFS, a measurable Docker performance gap to Linux remains for write-heavy workloads. The recommended approach for maximum performance on macOS is not to mount dependency directories like vendor/ (PHP) or node_modules/ (Node.js) as bind mounts, but to set them up as named Docker volumes instead. Docker volumes are managed entirely inside the Linux VM and are exempt from cross-VM synchronization. The source code itself keeps being mounted as a bind mount, since developers need to edit it directly.
# compose.yml: macOS performance optimization pattern
# Keep source code as bind-mount, dependencies as named volumes
services:
php:
image: php:8.4-fpm
volumes:
# Source code: bind-mount (developers edit this)
- ./src:/var/www/html/src:cached
# Dependencies: named volume (lives entirely inside the Linux VM)
- vendor:/var/www/html/vendor
- var:/var/www/html/var
- generated:/var/www/html/generated
node:
image: node:22-alpine
volumes:
- ./src:/app/src:cached
# node_modules as named volume, the biggest macOS performance win
- node_modules:/app/node_modules
volumes:
# All named volumes live in the VM filesystem, no APFS sync overhead
vendor:
var:
generated:
node_modules:
# Verify VirtioFS is active in Docker Desktop:
# Settings → General → "Use VirtioFS" must be checked
# docker run --rm -it alpine cat /proc/mounts | grep virtiofs
5. WSL2: between Linux and Windows
WSL2 (Windows Subsystem for Linux 2) brings a real Linux kernel to Windows, giving it a much better foundation for Docker performance than WSL1. Docker Desktop for Windows uses WSL2 as its backend, meaning containers run inside the WSL2 distribution. When project files live in the WSL2 file system (under /home/ or /root/), performance is nearly identical to native Linux, because file system operations stay within the Linux side of the system.
The classic WSL2 performance problem occurs when projects are stored in the Windows file system (under /mnt/c/ or /mnt/d/) and then mounted into Docker as bind mounts. In that case, file system operations have to cross the Plan 9 file system bridge between the Windows side and the Linux side of WSL2, similar to the macOS problem but even slower. The fix is straightforward: every project file that gets mounted into a Docker container must live in the WSL2 file system, not the Windows file system. That is the only path to good Docker performance under WSL2.
6. Optimizing bind mounts
Bind mounts are the most common cause of poor Docker performance in development environments. The single most important optimization is minimizing the scope of bind mounts. Instead of mounting the entire project directory, only mount the directories the container actually needs to read or write. A .dockerignore-style approach to Compose volumes helps define more precisely what gets mounted.
For directories the container writes to (cache, temporary files, build artifacts), a named volume is always faster than a bind mount. That holds true on every platform, but it is especially effective on macOS and on WSL2 when the project lives in the Windows file system. Another tuning option on macOS is the :cached mount flag: it lets Docker serve reads from a cache instead of always synchronizing directly with the APFS file system. Since VirtioFS was introduced, this flag has less impact than it used to, but it is still relevant for older Docker Desktop versions.
7. Resource limits and memory configuration
Docker Desktop on macOS and WSL2 shares CPU cores and RAM with the host system. The default Docker Desktop configuration often reserves fewer resources than would be sensible, which leads to swap usage and, in turn, to Docker performance problems. Development environments running several services (PHP-FPM, MySQL, Redis, Elasticsearch) should be allocated at least 6 to 8 GB of RAM. Swap inside the Docker VM is considerably slower than actual RAM and should be avoided by allocating enough memory up front.
Under WSL2, resource allocation is controlled through the .wslconfig file in the Windows user directory. That file lets you set limits for processors, memory, and swap. A WSL2 configuration that gives Docker enough resources is a prerequisite for good Docker performance on Windows. On macOS, configuration happens through Docker Desktop Settings, and there is no configuration file at the file system level.
# ~/.wslconfig: WSL2 resource configuration for Docker performance
# Place in C:\Users\<username>\.wslconfig on Windows host
[wsl2]
# Allocate sufficient memory for Docker workloads
memory=8GB
# Use half of available CPU cores
processors=4
# Reduce swap to force proper memory allocation
swap=2GB
# Improve kernel network performance
kernelCommandLine=net.ifnames=0
# Apply changes by restarting WSL2:
# wsl --shutdown (in PowerShell)
# Verify resource allocation inside WSL2:
# free -h
# nproc
# Docker compose.yml: resource limits per service
services:
mysql:
image: mysql:8.4
deploy:
resources:
limits:
memory: 2G
cpus: '2.0'
reservations:
memory: 512M
elasticsearch:
image: elasticsearch:8.13.0
environment:
# Critical: set heap to half of allocated memory
ES_JAVA_OPTS: "-Xms1g -Xmx1g"
deploy:
resources:
limits:
memory: 2G
8. BuildKit cache and layer optimization for faster builds
BuildKit has been the default build engine since Docker 23 and delivers substantial Docker performance improvements over the classic build engine. BuildKit parallelizes independent build stages, uses cache mounts for package manager downloads, and only writes changed layers to disk. For PHP projects, the most important tuning lever is the RUN --mount=type=cache instruction, which keeps the Composer or npm cache persistent between builds without writing that cache into the image layer.
A properly structured Dockerfile with BuildKit cache mounts cuts build time drastically: the first build installs all dependencies, and every subsequent build reuses the cache and only fetches new or changed packages. The order of Dockerfile layers determines how effectively that cache gets used. Layers that rarely change (system packages, base configuration) should come first, while layers that change often (application code, dependencies) should come last. Getting the layer order wrong forces Docker to rebuild every layer after a change on each code update, even when the dependencies themselves are unchanged.
9. Platform performance compared
A meaningful comparison of Docker performance across platforms has to account for the workload. Pure CPU benchmarks show barely any difference, while I/O-heavy workloads make the platform differences clearly visible.
| Platform | Bind Mount Performance | CPU Performance | Recommendation |
|---|---|---|---|
| Linux (native) | Baseline (100%) | Baseline (100%) | No tuning needed |
| macOS + VirtioFS | ~60-70% | ~95% | vendor/node_modules as named volume |
| macOS + gRPC FUSE | ~20-30% | ~90% | Upgrading to VirtioFS strongly recommended |
| WSL2 (files in WSL2) | ~95% | ~98% | Keep projects in the WSL2 file system |
| WSL2 (files in Windows) | ~15-25% | ~95% | Move projects to /home/ in WSL2 |
The percentages in the table are rough guidelines for typical PHP development environments with heavy file system usage (Composer, Symfony cache, Magento code generation). Plain web server benchmarks without bind mounts show much smaller platform differences. The key takeaway: platform performance is not a Docker problem, it is a file system problem, and it can be improved considerably with the measures described here.
Mironsoft
Docker Performance, Dev Environments, and CI/CD Optimization
Is your Docker development environment too slow?
We analyze your Docker Compose configuration, identify bind mount bottlenecks, and optimize builds for macOS, Linux, and WSL2, with measurable results.
Performance Analysis
Measure bind mount overhead and pinpoint bottlenecks in your Compose configuration
Build Optimization
BuildKit cache mounts, layer ordering, and multi-stage Dockerfiles for fast builds
Platform Setup
Optimize VirtioFS and WSL2 configuration for your entire development team
10. Summary
Docker performance is not a single, uniform problem, it is a platform-specific one. On Linux there is no virtualization overhead and bind mounts are native kernel mounts. On macOS, cross-VM file system synchronization creates the biggest overhead; VirtioFS reduces it significantly but does not eliminate it entirely. The most effective countermeasure is mounting dependency directories as named volumes instead of bind mounts. Under WSL2, performance depends entirely on where the project files live: in the WSL2 file system or in the Windows file system.
BuildKit cache mounts and a well thought out Dockerfile layer order reduce build times on every platform. Giving Docker Desktop sufficient RAM prevents swap usage, which would otherwise undo every other optimization. With these measures, Docker performance on macOS and WSL2 can reach a level that is perfectly acceptable for everyday development, without having to switch to Linux.
Docker Performance on macOS, Linux, and WSL: The Essentials at a Glance
Linux
Native Docker with no virtualization overhead. Reference platform for performance measurements. overlay2 as the storage driver.
macOS VirtioFS
Mount vendor and node_modules directories as named volumes. Keep VirtioFS enabled. Increase RAM to 6 to 8 GB.
WSL2
Keep project files exclusively in the WSL2 file system (/home/). Configure .wslconfig with sufficient RAM.
Builds
Use BuildKit with --mount=type=cache for Composer and npm. Layer order: stable layers first, code changes last.