Skip to main content
Cloud DevOps Solutions
Get a quote Talk to an engineer

Docker & Containerization

Docker Compose for Local Development: One Command, Whole Stack

A good Docker Compose setup lets any developer clone the repository and start the full stack, including databases, queues and the application with live reload, using one command. We design it to mirror production closely enough to catch real problems while staying fast on a laptop.

Key takeaways

  • The target is a new developer productive on day one with a single documented command.
  • Compose should run the dependencies and the app with hot reload, not a different architecture from production.
  • Healthchecks and ordered startup remove the flaky it-works-on-second-run problem.
  • Profiles keep optional services (observability, mocks) from slowing down everyone.
  • The Compose file is tested in CI so it does not rot.

The cost of a hard-to-start stack

Ask a new engineer how long it took to get the application running locally. If the answer is days, you are paying for it on every hire, every laptop replacement and every context switch between projects. The usual symptoms are a wiki page that has been wrong for a year, a database installed natively at a different version from production, and a Slack message that begins with “did anyone else get this error?”

Docker Compose fixes this when it is designed, not just written. It is a core part of our Docker containerisation service, and it pays for itself quickly.

What good looks like

  • git clone followed by one command starts everything, including seed data.
  • The versions of PostgreSQL, Redis and other dependencies match production.
  • Code changes appear without rebuilding the image by hand.
  • A second command resets the environment to a clean state.
  • Failure messages tell the developer what to do next.
  • The same Compose file (or a close relative) is used in CI for integration tests.

A practical layout

We split configuration so that each file has one job. A shared compose.yaml defines the stack, and an optional override handles local-only behaviour.

# compose.yaml
services:
  api:
    build:
      context: .
      target: dev
    environment:
      DATABASE_URL: postgres://app:app@db:5432/app
      REDIS_URL: redis://cache:6379
    ports:
      - "8080:8080"
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: package.json

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
      POSTGRES_DB: app
    volumes:
      - dbdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 5s
      timeout: 3s
      retries: 10

  cache:
    image: redis:7

  mailpit:
    image: axllent/mailpit
    profiles: ["tools"]
    ports:
      - "8025:8025"

volumes:
  dbdata:

The credentials above are throwaway local values that never leave the developer machine. Anything real stays out of the repository and is loaded from an untracked .env file or a secrets manager.

Start the default stack with docker compose up --watch, and add optional tools with docker compose --profile tools up.

The details that make it pleasant

Startup order and readiness

depends_on on its own only waits for a container to start, not for the service inside to accept connections. A healthcheck plus condition: service_healthy solves the common database race. We also make the application retry on startup, because production will have the same race during a restart.

Hot reload

Two approaches work. Bind mounts share your source directory with the container, and a watcher such as nodemon, air or Spring DevTools reloads the process. Compose Watch (available in recent Compose releases) syncs files or triggers rebuilds on change, which can be faster on macOS and Windows. We choose per project after trying both on your actual machines.

Seed data and resets

A make dev-reset that runs docker compose down -v and seeds known data turns a broken local database from a half-day problem into a thirty-second one. Seed scripts live in the repository and are tested in CI.

Local stand-ins for cloud services

LocalStack for AWS APIs, MinIO for S3-compatible storage and Mailpit for outbound email let developers work offline. We are cautious here: emulators drift from real services, so we use them for development speed and still run an integration test against real infrastructure in a pre-production environment.

Running tests and debuggers against the stack

A local stack earns more when it also runs the tests. We add a test profile or a separate service that runs the integration suite against the same database and cache, so a developer can reproduce a CI failure with one command rather than guessing at the difference. For debugging, the Dockerfile dev target exposes the language’s debug port, and the README shows the editor attach configuration for the two or three editors your team uses. Without that, people fall back to print statements and rebuilds.

Handling secrets and environment files

Local convenience is where secrets leak. We keep a committed .env.example with safe defaults and a .gitignore entry for the real .env. Where a service needs a real credential, such as a sandbox key for a payment provider, developers pull it from your secrets manager with a documented command rather than pasting it into chat. A pre-commit hook that scans for key patterns is a cheap safety net. This matters more than it looks: Compose files are routinely copied into other projects, and anything real inside them travels too.

Performance on different machines

Teams with mixed macOS, Windows (WSL 2) and Linux laptops hit different problems. Bind mounts across the virtualisation boundary can be slow for large dependency trees, so we keep node_modules or the language’s package cache in a named volume instead of the host directory. Apple silicon laptops pulling x86-only images run under emulation, which is slow, so we check that dependencies publish ARM variants or set the platform explicitly. These are small fixes, but they decide whether developers love the setup or quietly work around it.

Decisions and trade-offs

Full stack or only dependencies? Running only databases and queues in Compose while the app runs natively on the host is faster to iterate on. Running the app in a container is more consistent. We often offer both modes in the same file.

One Compose file for all services or one per repo? For a few services in a monorepo, one file works. For many independent repositories, an include: of per-service files or a shared dev environment repository scales better.

When not to do this. If your application depends on a large number of managed cloud services that cannot be emulated, a shared dev environment in the cloud may serve better than a heavy local stack. And Compose is a development tool: for running workloads on a cluster, see migrating from Docker Compose to Kubernetes or ECS.

Common failure modes

  • Port collisions with services already running on a laptop. We use documented, less common host ports.
  • Stale volumes carrying old schemas. A documented reset command fixes it.
  • File permission problems from bind mounts written by root. We run containers as a matching non-root user.
  • A dev image that differs wildly from production. We use a shared Dockerfile with a dev target, so the production stages remain the source of truth. See our Dockerfile standards.

Delivery and handover

We deliver the Compose files, a task wrapper, a README section, and a CI job that starts the stack from scratch and runs a smoke test. We then run a short onboarding session where a developer who has never seen the project follows the instructions, and we fix whatever slows them down.

How to tell it is working

A new joiner is running the system within the first hour, “works on my machine” reports fall, and CI uses the same definition developers use. To talk through your current setup, contact us.

References

How we deliver it

  1. 01

    Map the stack

    We list services, data stores, third-party dependencies and how developers currently run them.

  2. 02

    Define services and networks

    Each service gets an image or build context, environment, ports, volumes and a healthcheck.

  3. 03

    Add developer ergonomics

    Live reload or Compose Watch, seed data, one-command reset and sensible port conventions.

  4. 04

    Separate concerns

    A base file holds shared config, overrides handle dev-only behaviour, and profiles toggle optional services.

  5. 05

    Test it in CI

    A pipeline job starts the stack from scratch and runs a smoke test, so broken setups are caught early.

  6. 06

    Document and hand over

    A README section and a make or task wrapper cover start, stop, reset and troubleshooting.

Tools and technologies

Docker ComposeDocker DesktopCompose WatchPostgreSQLRedisLocalStackMinIOMailpitMakefileTaskfileGitHub ActionsGitLab CI

Frequently asked questions

What is Docker Compose used for in local development?
Docker Compose runs multiple containers as one stack from a single YAML file. For local development it starts your application alongside its database, cache, queue and any mock services, so developers do not install or configure them by hand. The same command works on every machine.
Should we use Docker Compose in production?
Usually not for anything with availability requirements. Compose is excellent for local development and for small single-host deployments, but it lacks the scheduling, rolling updates and self-healing of Kubernetes or ECS. When you outgrow it, we help with migrating Docker Compose to Kubernetes or ECS.
How do we make the app reload when code changes?
Either bind-mount the source directory and run a watcher inside the container, or use Compose Watch, which syncs or rebuilds on file changes. Bind mounts are simple but can be slow on some platforms; Compose Watch avoids that for many projects. We test both on your hardware mix.
How do we stop services starting before the database is ready?
Add a healthcheck to the database service and use depends_on with the condition service_healthy in the dependent service. This makes Compose wait for a real readiness signal instead of just container start. The application should also retry connections, because healthchecks alone do not guarantee readiness forever.
Our laptop runs out of memory with the full stack. What can we do?
Use profiles so developers start only what they are working on, run heavy dependencies as shared dev instances, and set sensible resource limits. We also review which services genuinely need to run locally and which can be mocked or pointed at a shared environment.

Talk to an engineer about docker compose local development

Book a free 30-minute call. We review your current setup and tell you honestly whether and how we can help.

Written by Saddique Khan, DevOps / SRE / Cloud Consultant Engineer. Last updated 2026-10-08.