on
Practical Docker Compose patterns for faster local microservices development
Local microservices development can quickly become slow and fiddly: dozens of services, slow image rebuilds, flakey startup ordering, and too much context switching. Docker Compose remains one of the simplest and most effective tools for working locally — but to stay productive you need a few practical patterns: start only the services you need, make builds fast and cache-friendly, manage readiness reliably, and keep your compose files organized for per-developer tweaks.
Below I walk through those patterns with concrete examples you can drop into a repo.
1) Run only the services you need with profiles
When working on one service in a 10-service system, spinning up the whole stack wastes CPU, memory and time. Compose profiles let you group services and enable only the subset you want for a session. Define a profile on a service, then pass –profile to docker compose.
Example snippet:
services:
api:
build: ./api
profiles: ["backend", "local-dev"]
worker:
build: ./worker
profiles: ["worker", "local-dev"]
postgres:
image: postgres:15
profiles: ["infra", "local-dev"]
Start only the backend services:
- docker compose –profile backend up -d
Profiles are an official Compose feature and are useful for dividing optional components (admin, debug, full-integration) from the core services you iterate on. (docs.docker.com)
2) Speed up iterative builds with BuildKit and inline caches
Rebuilding container images is the single biggest drag on edit→test cycles. Use BuildKit features to preserve and reuse build cache across builds and machines:
- Enable BuildKit (DOCKER_BUILDKIT=1) or use docker buildx.
- Export an inline cache when you push images (BUILDKIT_INLINE_CACHE=1 or –cache-to type=inline).
- On subsequent builds, import the cache via –cache-from or docker buildx options.
A small Compose-oriented pattern:
- In CI: build + push with inline cache metadata
- DOCKER_BUILDKIT=1 docker buildx build –push –build-arg BUILDKIT_INLINE_CACHE=1 –tag registry/myapp:dev .
- Local dev: pull last pushed tag and use it as cache-from when building.
Compose’s build settings let you pass build args, no_cache and other options; combining BuildKit cache export/import in CI with cache-from in local builds dramatically reduces layer rebuilds for unchanged steps. (docs.docker.com)
Tip: structure Dockerfiles so the frequently-changing bits (source code) are near the bottom and long-lived dependencies (apt installs, system setup) are early — that maximizes useful cache hits.
3) Make service readiness reliable: healthchecks + entrypoint wait loops
Startup order is a classic source of flakiness: your app container may start before the DB is accepting connections. Compose controls create/start order but doesn’t guarantee that the contained application is “ready” unless you use healthchecks. Add a HEALTHCHECK to the service that can validate readiness, and have dependents either wait for that health status or perform their own connect-and-wait logic.
Key points:
- Compose starts containers in dependency order, but “ready” is different from “running.” Compose will wait for a dependency’s healthcheck if you use the relevant compose features (service marked healthy), otherwise it only waits until the container process is running. (docs.docker.com)
- Because different compose spec versions and CLI implementations have slight differences, a reliable pattern is to add a small wait-for-it (or similar) script to the dependent service’s entrypoint. That script polls the host:port (or runs a simple SQL probe) until it succeeds, then starts the main process.
Example:
docker-compose.yml (excerpt)
services:
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: example
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 2s
retries: 10
api:
build: ./api
depends_on:
- db
entrypoint: ["/usr/local/bin/wait-for-db.sh", "pg:5432", "--", "/entrypoint.sh"]
The wait-for-db.sh approach makes your app resilient to varying startup times and avoids brittle race conditions in tests and local development.
4) Keep per-developer and per-environment tweaks out of the main file
It’s common to need different mounts, debug flags, or environment variables in local development. Use Compose override files and .env files to keep your base configuration clean while letting each developer or environment set ephemeral adjustments.
- Compose will automatically merge compose.yaml (or docker-compose.yml) with compose.override.yaml by default.
- Use a compose.override.yml (or a per-developer file added to .gitignore) to expose volumes, enable hot-reload, mount source code, or change ports without touching the base file.
Example: docker-compose.override.yml
services:
api:
volumes:
- ./api:/app
environment:
- DEBUG=true
ports:
- "3000:3000"
Merge behavior is documented in the Compose how-tos; it’s the standard way to keep a canonical base file and local extensions. (docs.docker.com)
5) Example: a compact dev workflow that ties these ideas together
- Compose files:
- compose.yaml — canonical stack (images, networks, volumes).
- compose.override.yml — local mounts, debug flags, hot-reload.
- Use profiles: start only api and local infra with docker compose –profile local-dev up.
- Build strategy:
- In CI: build and push images with inline cache metadata (BUILDKIT_INLINE_CACHE=1).
- Locally: pull latest dev images as cache-from before docker compose build.
- Readiness:
- Add healthchecks to DB and message-broker images.
- Add a short wait script in the api entrypoint to poll the DB before migrations run.
This combination reduces resource use, shortens rebuild times, and makes starts reproducible.
Troubleshooting quick hits
- Builds still slow: double-check Dockerfile layer order and that your .dockerignore is excluding node_modules/build artifacts.
- Healthcheck fails in Docker but works manually: check the healthcheck command’s environment and PATH inside the container (tools used by the probe must exist in image).
- Overrides not applied: verify which compose files you’re passing with -f or that your local override has the exact expected service names; use docker compose config to view the merged config.
Compose gives you the primitives — profiles, build options, healthchecks, overrides — and when you combine them intentionally you get a local development environment that’s fast, reliable, and easy for the team to use.
Further reading (official docs):
- Compose profiles and how to enable them. (docs.docker.com)
- Compose build options and build instructions. (docs.docker.com)
- BuildKit inline cache and cache exporters/importers. (docs.docker.com)
- How Compose handles startup order and healthchecks. (docs.docker.com)
- Multiple Compose file merging / override behavior. (docs.docker.com)
Putting these patterns into practice pays off quickly: faster builds, fewer spurious failures, and a more pleasant feedback loop while you develop microservices locally.