Tyagi // Engineering Log
← Back to Codelabs

Codelab

Docker for Local Development: A Practical Workflow

A docker-compose setup for local dev that stays fast — bind mounts, layer caching, and the tradeoffs that actually matter day to day.

Last updated 2026-06 · 3 min read Intro
dockertoolinglocal-dev

One compose file, not one Dockerfile per service guessing

For local dev, docker-compose.yml should be the source of truth for how services talk to each other — ports, env vars, dependency order via depends_on with healthchecks, not condition: service_started alone (which doesn’t wait for the service to actually be ready).

services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 2s
      retries: 10
  api:
    build: .
    depends_on:
      db:
        condition: service_healthy

Bind-mount source, not the whole project

Mounting the entire repo into a container is the easiest way to make builds slow — node_modules or .dart_tool written by the container’s architecture can conflict with what’s on the host. Mount only the source directories that need hot-reload, and let dependency directories live inside the container’s own filesystem layer via a named volume.

Layer order determines rebuild speed

Dependency installation should happen in a layer before copying application code:

COPY package.json package-lock.json ./
RUN npm ci
COPY . .

Reversed, every code change invalidates the dependency-install layer and turns a 2-second rebuild into a multi-minute one.

Don’t run databases in Docker for every workflow

Docker is excellent for disposable, reproducible service dependencies (Postgres, Redis, a mock SMTP server). It’s a worse fit when you need to attach a native profiler or debugger that expects a local process — know when to drop back to a host-installed instance instead of fighting the container boundary.

Tear-down hygiene

docker compose down -v between schema changes avoids the classic “works after I delete my volumes” debugging session. If migrations are flaky, that’s usually the actual signal, not bad luck.

Healthchecks are worth the extra few lines

It’s tempting to skip healthchecks and just add a sleep before the dependent service starts, but a fixed sleep either wastes time when the dependency starts fast or fails intermittently when it starts slow — neither is what you want in a workflow you’ll run dozens of times a day. A healthcheck with a short interval and a reasonable retry count converges on “as fast as possible, but always correct” instead of picking one or the other, and it keeps working even as the dependency’s startup time changes across machines or container runtimes.

Treat compose as documentation, not just orchestration

Beyond actually running the stack, a well-written docker-compose.yml doubles as the clearest record of how services are supposed to relate to each other — which service depends on which, what ports are exposed, what environment variables are required. Keeping it accurate is cheap compared to the alternative of a new contributor reverse-engineering the dependency graph from README fragments and tribal knowledge.

Share: X LinkedIn

Related topics

Keep exploring

Setting Up a Flutter Development Environment on Linux

A practical setup for Flutter + Dart on Linux — SDK install, editor config, emulator, and the gotchas that waste the first hour.

#flutter#dart#linux