Every new engineer who joined my last team spent their entire first day fighting local environment setup, a Postgres version mismatch here, a missing Redis instance there, one person on an M1 Mac hitting an architecture-specific dependency error nobody else on the team had ever seen. I lost count of how many Slack threads started with "works on my machine." Building a proper Docker Compose setup didn't happen because it seemed like good practice, it happened because I was tired of being the unofficial onboarding support desk.
My first attempt tried to containerize everything, including the application code itself, with hot reloading through a bind mount. This technically worked but file change detection through Docker's bind mounts on macOS was noticeably slower than running the app natively, a change that took under a second locally took closer to four or five seconds to trigger a reload inside the container. I abandoned that approach after a week of complaints and settled on containerizing only the infrastructure, Postgres, Redis, and a mail-catching service for local email testing, while running the actual application natively on the host machine.
services:
postgres:
image: postgres:16.3
environment:
POSTGRES_PASSWORD: localdev
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7.2-alpine
ports:
- "6379:6379"
mailhog:
image: mailhog/mailhog
ports:
- "8025:8025"
- "1025:1025"
volumes:
pgdata:
Before this setup, our README said "Postgres 15 or later," which sounds reasonable and caused real problems, since a subtle behavior difference between Postgres 15 and 16 around a specific JSON aggregation function broke a query for one engineer and not another, and we spent half a day debugging what turned out to be a version difference nobody had flagged. Pinning the exact image tag, not just the major version, eliminated that entire category of bug.
An empty database on first run meant every new engineer had to manually create test accounts, products, and orders before they could see anything meaningful in the app, a genuinely tedious first hour. I wrote a seed script that runs automatically against the Postgres container on first boot, populating realistic-looking fake data, generated with a library rather than hand-written records, so a fresh clone of the repo produces a genuinely usable local environment within about ninety seconds of running one command.
Early on, I had the Postgres data directory NOT mapped to a named volume, meaning every time someone ran docker compose down, their entire local database wiped clean, including test data they'd been building up manually for hours. One engineer lost an afternoon's worth of manually created test scenarios this way before I realized the volume mapping was missing, added it, and never heard that specific complaint again.
The application would sometimes fail to connect to Postgres on a fresh docker compose up because the app started trying to connect before Postgres had actually finished initializing inside its container, a timing issue that was inconsistent enough to be genuinely confusing to debug. Adding a proper health check and a depends_on condition waiting for that health check to pass eliminated the flakiness entirely.
services:
postgres:
image: postgres:16.3
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
app:
depends_on:
postgres:
condition: service_healthy
New engineer onboarding went from a full first day of environment troubleshooting to something closer to twenty minutes, clone the repo, run docker compose up, run the seed script, start the app. I still get the occasional Slack message about local setup, but it's now genuinely rare rather than the default first-day experience, and I stopped being the unofficial onboarding desk almost entirely, which was honestly the actual goal the whole time.