Docker Compose for local development: where it breaks
A developer starts the project with a single command, everything is green for them — but the same commit fails on migrations in the shared environment.
The cause is usually not the code: in the compose file, services start in parallel, and the dependency is described as “start the container” rather than “wait until it is ready.” The database accepts connections before it finishes initialization, the application writes to a still-empty schema, and silently loses some data.
This is not visible locally, because the database on the developer's machine is already up from a previous run.
The cost of such environment divergence is not a single failed run, but unreproducible defects.
Half of the “it works on my machine” requests are closed after hours of manual comparison: image versions are not pinned and a fresh branch is pulled, variables live in a personal file outside the repository, and data volumes are left over from a previous task.
Starting the project on a new machine turns into a ten-step instruction that each person follows their own way — build reproducibility rests on a specific person's memory, not on configuration.
A separate pain point is configuring service dependencies: startup order, readiness waits, shared volumes, seeds for test data. As long as this is described in words in chat rather than in the compose file, every new machine is a lottery.
We will look at how this is addressed: which readiness checks to set, how to pin versions and move configuration into the repository so that docker compose up gives the same result for everyone.
What a declarative description of the service environment provides
A single description of services replaces a ten-step manual instruction: an engineer clones the repository, starts the environment with a single Docker Compose command, and gets a working stack — database, message broker, cache, and the application itself.
Time to first launch drops from days of waiting for a configured environment to 20–30 minutes, and manual installations on local machines almost completely leave the process.
The service layout lives in one file: major image branches, environment variables, data volumes, networks, and dependency order.
Not only the composition is pinned down, but also mutual addressing — the application reaches the database by the same name as in the shared environment, so the code does not become overgrown with branches for “a different address on the local machine.”
This closes a separate class of failures — version desynchronization and incorrect startup order.
A readiness check together with a health check prevents migrations while the database is still only accepting connections; a major version mismatch between client and server is caught at startup, not at the end of the week on the shared stand.
Handing over the environment between engineers stops being a separate job: the file and the list of pinned versions sit next to the code, a new person on the team reaches the working cycle on the first day, without a chat asking “which versions do you have?” The practical effect is visible in the share of defects that reproduce locally — it grows from half to most, and triage happens on the developer's machine, not on the shared stand.
Ways to run the environment: comparing modes
The run mode determines not the convenience of an individual developer, but how the team reproduces dependencies: image versions, networks, volumes, environment variables, and service startup order.
In practice, four options have emerged — from running services locally on a workstation to manual installation — and teams almost always mix them together.
The selection criterion is short: where the service state lives and who is responsible for restoring it. The more dependencies run next to the workstation, the higher the machine requirements and the more noticeable the divergence between colleagues' environments.
| Option | When it fits | Limitations |
|---|---|---|
| Local service startup | up to 5–7 services, team up to 10 people, need to work offline | workstation resources, image versions diverge between participants |
| Shared remote stand | many heavy services, need unified data for checks | network latency, shared data state, stand downtime stops everyone |
| Hybrid development mode | some services change often, some are stable | coordinated addresses and access are required, diagnostics are more complex |
| Manual service installation | one-off experiments, one or two services | not reproducible for others, a newcomer spends days on setup |
The cost of maintaining the environment is measured in engineer hours: manual installation and diverged local copies consume them during onboarding and when investigating “it doesn't work for me.” The hybrid mode gives the best balance when the configuration file explicitly pins down which services run locally and which go to the stand — then local startup remains predictable, and the stand does not turn into a dump of branches.
Implementation order: from audit to handover to the team
We structure the work so that the environment starts the same for everyone — for a new engineer and on the build server.
We start not with the compose file, but with an inventory: until the composition of services and the connections between them are described, automation cements the current disorder.
- Service inventory. We collect everything that runs locally: the application, database, cache, queues, background workers, stubs for external systems. For each, we pin down the image source, volumes, and environment variables — this reveals components that live only on one developer's machine.
- Dependency diagram. We mark who waits for whom at startup and over which ports the exchange happens. This forms the startup order and readiness checks — without it, the environment does not come up on the first try.
- Environment description. We move the composition and parameters into a single description: safe defaults, secrets from a local file outside the repository. Settings stop being oral tradition.
- Preparation automation. One command starts the environment, populates the database with test data, and runs migrations. Starting on a new workstation takes minutes instead of hours, and dependency versions match for everyone.
- Clean-machine check. We deploy on a new system and on the build server without manual preparation. This catches dependence on forgotten local files and version mismatches before handover.
- Handover of the regulations to the team. We provide the structure description, rules for adding a service, the procedure for updating dependencies, and typical operations. The team maintains the environment itself; we get involved for complex changes.
Set of artifacts: configurations, diagrams, regulations
After the work is completed, the repository contains not a set of verbal manual actions but a reproducible kit: the environment starts with a single command on a clean machine, connections between services are described explicitly, and agreements do not live in chat. Below is what we pin down in the repository and in internal documentation.
- Environment description files — the main Docker Compose manifest, a variable template with safe defaults, and a separate local override file that is not committed.
- Service relationship diagram — which containers see each other, where volumes are mounted, which ports are exposed outward, who starts in what order, and who depends on whom.
- Secret access policies — what is passed through environment variables, what is pulled from a secret store, what never enters the repository; exception rules and the procedure for reviewing them.
- New developer instructions — from cloning to first launch and test run, with a list of typical failures and the signs by which to recognize them.
- Configuration update regulations — who is allowed to change the base file, how a change is validated, how it is rolled back if the environment stops starting.
- Set of automated checks — validation of syntax and configuration compatibility in the main branch, so that a broken description does not spread across the team's machines.
- Volume and data map — what is preserved between restarts, what is cleared, how to return the environment to a clean state for a repeatable test.
The kit is considered ready when a new engineer starts the environment without questions for the author, and updating the configuration does not require verbal explanations.
Case study: how environment divergence was eliminated in a team
A product team of eight engineers came to us: seven services in one repository, a shared database, cache, and queue. Everyone started the environment according to a wiki instruction — installing dependencies, local servers, tweaking configs for themselves. Startup took from forty minutes to several hours, and for a new person — almost a day.
The root cause turned out to be not laziness, but version drift and startup order. Everyone had their own set of database and cache versions, services started without waiting for the storage to be ready, and migrations were applied manually and in different sequences. Hence the recurring defects: “works locally, fails in the shared environment.”
We described the environment in Docker Compose: a single file plus overrides for the local profile, pinned major image versions, readiness checks and startup dependencies, named volumes for data, isolated networks, and automatic migration application with a test data set at startup.
What changed in numbers
- Environment startup: was 40–60 minutes, became 6–8 minutes with a single command, without reading the wiki.
- Share of runs failing due to environment divergence: was roughly every third, became isolated cases.
- Onboarding a new engineer: was about a workday, became 40–50 minutes to the first running service.
- Failures of the “service started before the database” class stopped reproducing: the order is set by dependencies and readiness checks, not by discipline.
The test environment became reproducible: the same file runs on the developer's machine and in the shared environment, with differences moved into environment variables.
Recurring defects disappeared not because someone became more careful, but because there was nowhere left to get divergence from.
What to do if the environment is already assembled manually
Yes, the transition does not require stopping current tasks: the compose file is assembled service by service, and manual installation continues to work while only part of the environment is described in it.
Start with what is already isolated — the database, cache, message queue. They are described in Docker Compose, the major image version is pinned, and the port, credentials, and data path are moved into environment variables.
For local development, this is enough: an engineer starts the service as a container or keeps their own installation, and the application startup commands do not change.
Next, it is important to preserve familiar workflows: the debugger, initialization scripts, access inside the container, port publishing to the host.
Until this is reproduced, the service is left in its previous mode — a partial description is a normal intermediate state, not an unfinished job.
The main transition risks are version mismatch between the local installation and the image, port conflicts, and data left outside volumes.
The first is treated by pinning the image version, the second by moving ports into variables, the third by named volumes and explicit mount paths: otherwise, state is lost when the container is recreated.
The check is simple: a new engineer starts the environment with a single command and gets the same versions, ports, and data as everyone else. When this holds for all services, manual installation leaves the onboarding instructions.
Technical concerns and limitations of the approach
A local stand does not eliminate external dependencies: a payment gateway, message broker, and cloud services cannot be run in a container — they are covered by a test environment or a stub.
Therefore, we keep profiles in the configuration: the base set starts on any machine, and what requires external access is enabled with a separate flag.
Separate limitations are secrets, dependency versions, and workstation resources. We do not store secrets in the repository: only a local manager or a file excluded from version control.
Versions are pinned by base images and lock files; otherwise, environment divergence surfaces during debugging. And the full set of services on a weak laptop goes into swap — profiles and memory limits help here.
What to do if some services cannot be run locally?
We build a hybrid scheme: what is critical for development goes in containers, external things go through a test environment with limited permissions. The developer gets a working scenario without gaining access to the production environment.
How to pass secrets and access to the team?
Only a template with empty values is in the repository; real ones are substituted locally. Access rotation does not require configuration changes, and a departing employee does not take the whole stand with them.
How demanding is the environment on machine resources?
Profiles, per-container memory limits, and lightweight base images help: startup becomes predictable, and the build remains reproducible on machines of different classes.
Let's discuss the task and come back with a plan and estimate
To make the estimate substantive rather than “plus or minus a week,” it helps to send a set of inputs — we will use them to assemble a work plan and give timelines. Nothing needs to be prepared specially: what is already in the repository and in the team's heads is enough.
- a link to the repository or service tree: which applications start together, what communicates over the network with what, and where dependencies are external;
- current
docker-compose.ymland Dockerfile, even drafts — it is immediately clear where the environment diverges from prod; - a list of environment variables and how secrets are handled: what currently sets access, what must not be moved into a shared file;
- a description of environments: what should start on the developer's machine, what on the test stand, and how they differ;
- constraints: which OSes and machines developers have, and whether there is a requirement to start the environment with a single command without manual steps;
- state requirements: whether pre-filled data is needed, and how schema migrations and volumes relate between runs;
- bottlenecks that already get in the way: long first startup, port conflicts, “it works for me, not for you.”
Then the order is simple: we discuss the task and boundaries — where local startup ends and infrastructure begins. Then we come back with a work plan, team composition, and implementation timelines broken down by stage, so you can start some changes without waiting for the end. The next step is a short conversation on the inputs and agreeing on the plan.







