Practical Docker Compose Patterns for local microservices: profiles, overrides, and reliable startup

Docker Compose is a great tool for running a multi-service application on your laptop. But when your local microservices stack grows (API, DB, cache, dev tools, migration tasks), a single docker-compose.yml can quickly get messy — and services may race to start before their dependencies are ready. This article shows pragmatic patterns you can apply today: using profiles to gate dev-only services, override files for local tweaks, and a reliable startup strategy combining healthchecks, depends_on, and small “wait” tools when needed.

Why these patterns?

Profiles: gate dev tooling without duplicating YAML

Example (snippet):

services:
  api:
    build: ./api

  postgres:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: example

  adminer:
    image: adminer
    profiles: ["dev-tools"]        # only start in dev when profile enabled

Start everything for normal development:

Start dev tools too:

Or run a single tools service (auto-enables its profile):

Use case: Put database GUI, mock services, storybook, and migration helpers behind profiles. Your teammates can run a minimal stack quickly, and opt-in for extra tools only when needed.

Override files: keep local changes out of main config

Example override (compose.override.yaml):

services:
  api:
    volumes:
      - ./api:/app           # live code changes in dev
    environment:
      - DEBUG=1
    ports:
      - "3000:3000"

Healthchecks + depends_on: control startup order more reliably Compose has two helpful pieces for startup ordering:

Example: wait for Postgres to accept connections before starting api

services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: example
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"]
      interval: 10s
      timeout: 5s
      retries: 6
      start_period: 10s

  api:
    build: ./api
    depends_on:
      postgres:
        condition: service_healthy

Notes and gotchas:

When healthchecks aren’t enough: wait-for tools Sometimes a service is “listening” but not yet able to perform specific work (e.g., migrations not applied, initialization scripts running). For those cases, two practical options:

1) Make the dependent service resilient: add retries/connect-backoff in code or an entrypoint script that retries until it can do what it needs.

2) Use a small wait tool (wait-for-it.sh, dockerize, wait-port) to block the container’s main command until a specific TCP/HTTP endpoint or file exists. These are simple and effective for local dev when you don’t want to change app code. Docker’s docs and community answers commonly recommend wait-for-it or dockerize for these use cases. (stackoverflow.com)

Example: using dockerize in a server image’s entrypoint

FROM python:3.11-slim
# install dockerize binary, or use jwilder/dockerize base
COPY dockerize /usr/local/bin/dockerize
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

entrypoint.sh:

#!/bin/sh
# wait for postgres to accept TCP connections
dockerize -wait tcp://postgres:5432 -timeout 60s
exec gunicorn app:app

Pattern: migrations as a profile-backed one-off A common pattern is to keep migrations as a dedicated service that runs the migration command (instead of running migrations automatically on app start). Make it part of a profile like tools or ci so it doesn’t run every time you bring the stack up.

Example:

services:
  db-migrate:
    image: myapp:latest
    command: ["./migrate.sh"]
    depends_on:
      - postgres
    profiles: ["tools"]

Run migrations explicitly (this automatically enables the profile for that run): docker compose run –rm db-migrate. This avoids surprising schema changes during dev hot-reload and lets CI execute migrations in a controlled step.

Putting it together: a simple local workflow

  1. Start the core stack for fast iteration:
    • docker compose up -d
  2. When you need tooling (admin UI, mock servers), enable the profile:
    • docker compose –profile dev-tools up -d
  3. To run migrations or one-off tasks:
    • docker compose run –rm db-migrate (or) docker compose –profile tools up db-migrate
  4. When debugging a service with live code edits, use compose.override.yaml to mount source and expose ports; keep that file out of production configs.

Additional tips

Closing: make Compose work for you, not against you Docker Compose gives several orthogonal tools — profiles, mergeable override files, healthchecks, depends_on behaviors, and small wait utilities. Combined thoughtfully, they let you model a real microservices development experience that’s fast to start, easy to customize per-developer, and reliable enough to avoid “it works on my machine” races.

Key references

If you want, I can: