It Took a Senior Engineer Two Days to Run Our App Locally
A client had fourteen microservices, a 280-line Docker Compose file, and an wiki page last updated eight months ago. Their newest hire spent two days just trying to get the app running on her laptop.
I was three hours into a consulting engagement at a logistics company when their newest hire, a senior engineer with twelve years of experience, leaned over and said: "I've been here four days and I still can't run the app."
She wasn't exaggerating. The onboarding doc pointed to a wiki page that referenced a Docker Compose file that needed six environment variables from a .env.example that hadn't been updated since December. Two of the services required a VPN connection to a staging database because nobody had figured out how to seed local data. One service needed a specific version of Node that conflicted with what three other services expected.
She'd spent two full days on this. She wasn't stuck because she was inexperienced. She was stuck because the setup was genuinely broken.
The 280-line Docker Compose file
I asked to see the docker-compose.yml. It was 280 lines long and defined fourteen services: a React frontend, a BFF layer, four domain services, two worker processes, PostgreSQL, Redis, RabbitMQ, Elasticsearch, a mock SMTP server, and a "local auth proxy" that someone had built because the OAuth provider didn't support localhost callbacks.
Every service had its own Dockerfile. Half of them had COPY . . without a .dockerignore, so a full rebuild meant copying node_modules into the build context. Starting the entire stack from scratch took somewhere between eight and fourteen minutes, depending on your machine. On the new hire's M2 MacBook Air, one of the services segfaulted during startup because of an ARM compatibility issue in a native dependency nobody had pinned.
The coping mechanisms
When I talked to the existing engineers, a pattern emerged: nobody actually ran all fourteen services. Each person had their own subset. The frontend developer ran the React app and the BFF, and pointed everything else at the shared staging environment. The backend engineers each ran their own service and maybe PostgreSQL, mocking everything else with hardcoded responses. One engineer had a set of shell scripts called just-my-stuff.sh and full-stack-lol.sh.
This meant that integration issues only surfaced after code hit staging. The team had a Slack channel called #staging-broken with 340 unread messages. It had been broken more often than not for the past two months.
Nobody questioned this. It was just how things worked.
What we changed
We didn't rip everything apart. The team was shipping features and couldn't afford a month-long infrastructure rewrite. Instead, we made three targeted changes over two weeks.
First, we split the Compose file into profiles. Docker Compose supports profiles natively, and it's one of those features that solves an obvious problem but nobody seems to use. We defined three: core (the databases and message broker), backend (all domain services), and full (everything). A frontend developer could run docker compose --profile core up and have just the infrastructure, then run the BFF and frontend natively. Backend engineers could run docker compose --profile backend up and skip the frontend entirely.
Second, we wrote a seed script that actually worked. The old one referenced tables that had been renamed four months ago. We wrote a new one that created a realistic local dataset — a handful of orders, some inventory, a test user with known credentials — and added it to the core profile so it ran automatically on first startup. No more VPN to staging for local data.
Third, we deleted two services. The notification service had been functionally dead for three months — it sent emails to a mock SMTP server in development and had been turned off in production after they switched to a third-party transactional email provider. The second worker process duplicated queue consumption logic that Worker 1 already handled. Its original author had left. Nobody knew why it existed. We checked the logs: it had processed zero messages in the last 90 days.
After these changes, a cold start on the core profile took under two minutes. The new hire got the app running in about twenty minutes using the updated onboarding doc, which we also rewrote.
The quiet cost
What struck me about this engagement wasn't the technical complexity. Fourteen services is a lot, but I've seen worse. What struck me was how long the team had lived with a broken local setup without treating it as a problem worth fixing.
They'd adapted. They'd built workarounds. Each engineer had carved out their own functional little island and stopped expecting the whole system to run on one machine. The cost was invisible because it was distributed: five minutes here waiting for staging to deploy, twenty minutes there debugging an integration issue that would have been obvious locally, a full day lost when a new person joined.
I asked the engineering manager if he'd ever tracked how much time the team spent fighting their local environment. He hadn't. When we rough-estimated it, we landed at something like 15-20 hours per week across the team. For context, that's nearly a full headcount spent on local dev friction.
Note
The hardest part wasn't the fix. It was getting the team to see the broken setup as something fixable rather than an immutable fact of life. How many teams have you worked on where "it takes a while to get set up" is just accepted wisdom?