Local microservice stacks are great for testing interactions, but slow image builds and noisy file-syncing can turn your inner loop into a drag. Two modern Compose-era features give you high leverage: the Compose “develop” section (watch/sync/rebuild) that targets development workflows, and BuildKit cache mounts (RUN –mount=type=cache) that dramatically cut rebuild time for package installs and compiles. This article explains how to combine them safely with compose volumes and bind mounts to get faster, more reliable local iteration. (docs.docker.com)
Compose now includes an optional develop subsection for services. When enabled it lets Compose watch paths for changes and take actions tailored for development: rebuild an image, restart a container, synchronize files into a running container, or synchronize then exec a command. These actions let you update code without rebuilding everything or losing service state. The feature is available in Docker Compose v2.22.0 and later. (docs.docker.com)
Highlights:
The Compose CLI also exposes watch-related commands and profile mechanisms to combine dev-only services (e.g., debuggers) with production-like services. (docs.docker.com)
BuildKit provides the RUN –mount=type=cache feature inside Dockerfiles. For language ecosystems where dependency installs dominate build time (npm, pip, gem, go build cache), cache mounts let those directories persist across builds without baking them into the image. That speeds rebuilds and reduces network fetches. Typical use: RUN –mount=type=cache,target=/root/.cache/npm npm ci (or the appropriate cache path for your package manager). (github.com)
Important implementation note: cache mounts are stored by the builder locally and usually are not part of exported cache layers (so CI builders that start fresh each run may see empty cache mounts unless you configure cache export/import). Use cache mounts mainly to speed local developer builds where the same builder/runner is reused. (github.com)
A common stumbling block: a host bind mount over a non-empty directory in the image will obscure the container’s existing contents. That means an npm install done during image build can appear to “disappear” when you bind-mount your project root over /app. The engine behavior is normal, but surprising if you expect the built node_modules to stay available. (docs.docker.com)
Workarounds:
Named volumes are persistent and created by Compose on demand; Compose’s volumes model is designed for this kind of separation between host code and container-managed runtime files. (docs.docker.com)
Example docker-compose.yml (illustrative):
services:
frontend:
build: ./frontend
develop:
watch:
- path: ./frontend/src
action: sync
target: /app/src
ignore:
- node_modules/
- path: ./frontend/public
action: sync
target: /app/public
volumes:
- ./frontend:/app:delegated
- frontend-node-modules:/app/node_modules
backend:
build: ./backend
develop:
watch:
- path: ./backend
action: rebuild
volumes:
- ./backend:/srv/app
volumes:
frontend-node-modules:
Example Dockerfile (uses BuildKit cache mounts):
# syntax=docker/dockerfile:1
FROM node:18-alpine
WORKDIR /app
# cache npm cache between builds
RUN --mount=type=cache,target=/root/.npm \
npm ci --silent
COPY . .
CMD ["npm", "start"]
Notes:
Combining Compose’s develop model (fine-grained watch and sync rules) with BuildKit cache mounts gives you an efficient local inner loop: small file edits get synchronized into running services, package installs are cached between builds, and heavyweight rebuilds are reserved for real changes. Use named volumes for runtime-only directories (like node_modules) so bind mounts don’t accidentally mask built artifacts, and remember that cache mounts are a local optimization — treat CI cache strategy separately. (docs.docker.com)