How to build a reproducible local environment with Docker Compose for databases, MinIO, AWS/Azure/GCP emulators and WireMock, and what trade-offs it has.
Testing integrations against real services is slow, costly and sometimes dangerous. I built a repository, dev-tools, that gathers in one place everything needed to set up a local environment and test without depending on real services:
- Local databases and storage with Docker Compose.
- AWS, Azure and GCP emulators.
- A mock server with WireMock.
- Sample data in JSON.
This article explains the idea, how to use it and, above all, its limits.
The Problem
A typical project depends on a database, object storage, sometimes cloud services and third-party APIs. Without a consistent local environment, familiar things happen:
- "It works on my machine", because each developer installed different versions.
- Tests that depend on someone else's sandbox that goes down or has usage limits.
- Real credentials on development machines, an unnecessary risk.
- Slow onboarding: days to get the environment working.
The solution is to declare the environment as code and bring it up with one command.
Repository Structure
Each folder is independent: you only use the one you need.
dev-tools/
├── dev-infra/ # Local databases and storage
├── floci/ # AWS / Azure / GCP cloud emulators
├── wiremock/ # Mock server + Postman and Bruno collections
├── demo/ # Sample data in JSON
dev-infra: Databases and Storage
A Compose file with MinIO, PostgreSQL, MySQL, Redis, Oracle, MongoDB and SQL Server, with a development user created in each service. Credentials live in a local .env that isn't versioned; only the .env.example template is published.
floci: Cloud Emulators
Local emulators for AWS, Azure and GCP with a unified web console, using the images from the open-source Floci project. They let you develop against the S3, DynamoDB, Lambda and other service APIs without a real account or costs.
wiremock: Third-Party API Mocks
A WireMock server with an admin interface, sample stubs and Postman and Bruno collections. It's used to simulate external APIs: successful responses, errors, latencies and edge cases that are hard to trigger on the real service.
demo: Sample Data
Bilingual JSON catalogs (countries, banks, professions…) to use in seeds, mocks and tests.
How to Use It
git clone https://github.com/ifirestone/dev-tools.git
cd dev-toolsLocal credentials (not versioned)
cp dev-infra/.env.example dev-infra/.env
Each stack is independent
docker compose -f dev-infra/docker-compose.yml up -d
docker compose -f floci/docker-compose.yml up -d
docker compose -f wiremock/docker-compose.yml up -d
The three stacks don't share ports, so they can run at the same time. To shut one down:
docker compose -f <folder>/docker-compose.yml down # keeps data
docker compose -f <folder>/docker-compose.yml down -v # also deletes data
An example with the AWS emulator:
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=us-east-1
aws --endpoint-url http://localhost:4566 s3 ls
Your application code only changes the endpoint. The rest of the client is the same one you'd use in the real cloud.
Why Mocks in Addition to Emulators
Emulators cover services with a public, stable API. For specific third-party APIs (a payment provider, a corporate platform) there is no emulator, and that's where WireMock helps:
- Determinism: the response is always the same, so tests aren't brittle.
- Error cases on demand: a 500, a timeout or a malformed response can be simulated in seconds.
- Parallel work: you can develop the integration before having access to the provider's sandbox.
- Explicit contract: the stubs document how you expect the third party to respond.
Trade-offs and Limits (What You Should Know)
A toolbox like this has costs, and they're worth stating:
- Partial fidelity. An emulator doesn't replicate 100% of the real cloud's behavior: permissions, quota limits and edge cases may differ. Use it for development and quick tests, not as a substitute for testing against the real service before production.
- Resource consumption. Oracle and SQL Server use a lot of memory. If your machine is tight, bring up only the services you need.
- Local development only. None of this is meant for production or for exposure on a shared network. The sample credentials are public.
-
Docker access. The emulators mount
/var/run/docker.sockto support services such as serverless functions, which gives them control over Docker on your machine. Remove that mount in environments where you don't need it. -
Images tagged
latest. Several services use that tag. If you need reproducibility, pin versions. - Apple Silicon. Some images only exist for x86_64 and run emulated; you'll see a platform warning, not an error.
What I Would Do Differently With More Time
- Pin the versions of all images by default.
- Add Compose profiles to bring up typical combinations (for example, "just PostgreSQL + MinIO").
- Automate health checks to know when each service is ready.
Conclusion
- A local environment declared as code speeds up onboarding and eliminates "it works on my machine".
- Emulators and mocks let you test integrations without depending on third parties or exposing real credentials.
- Use them for development, with clarity about their fidelity and security limits.
The repository is open source (MIT license). If you use it and a service is missing, contributions are welcome.
Top comments (1)
Declaring the whole environment as code is what makes this more than a convenience — the onboarding and version-drift problems you list are the same problem, a setup that lives in people's heads instead of in the repo. Keeping real credentials off developer machines is the benefit that gets undervalued until an incident prices it. The limit I would underline is that emulators and WireMock verify the shape of an integration, not the behavior of the other side: a mock returns what you told it the partner returns, so one scheduled pass against the real sandbox still has to exist, or the local suite can be green about an assumption nobody ever tested.