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.
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.