How to run a microservice environment locally
Running one microservice locally is easy: its README says how. The trouble starts when the flow you want to test goes through four of them, and each one calls two more that are not yours.
This page is not a tutorial for one particular tool: it is which pieces you need, why the hard part is not the one it looks like, and how each one gets solved — with Aseptic at the end, because that is what this site documents, but the four problems are the same whatever you use.
The four pieces
Section titled “The four pieces”A local environment worth having needs to solve four things. In this order of real difficulty, which is almost never the order they get tackled in:
- Shared infrastructure. Kafka, Redis, PostgreSQL, whatever your system uses. It is the most visible part and the most solved: containers.
- Starting each service. Its command, its profile, its variables, its port. Repetitive work, but mechanical.
- Communication between services. Where each one calls. This is where the morning goes, and it is what almost nobody talks about.
- Data. A schema created and something inside it to test with.
1. The infrastructure
Section titled “1. The infrastructure”A docker-compose.yml with the shared pieces and docker compose up. It is what
everybody does and it is right: containers are exactly the correct tool for this.
Two details that bite later:
- Ports collide. Every team declares 5432 and 6379. As soon as you have two projects open, or you bring things up and down, the conflicts start.
- Infrastructure is shared between projects; data is not. Reusing the same Postgres for two different systems ends in a schema nobody knows the owner of.
2. Starting each service
Section titled “2. Starting each service”Every service has its own way: ./mvnw quarkus:dev,
./gradlew bootRun -Dspring.profiles.active=local, npm start, a binary. With different
profiles, variables and flags.
This is what usually ends up written in a forty-step LOCAL_SETUP.md: it works, but it
ages badly. Nobody updates the document when a flag changes, and the next person to
arrive discovers the gap through errors.
One detail that decides far more than it looks: not every service wants to run the same way. The one you are working on you want native, with hot reload and the debugger attached. The other three only have to be up: containerised is fine, isolated and without cluttering your machine.
3. Communication: the hard part
Section titled “3. Communication: the hard part”Here is the real work, and it is the one no container tool solves for you.
Your bff service calls orders. In production that URL comes from service discovery or
the ingress. Locally, someone has to decide where it points, and the options are not
equivalent:
- At the service running on your machine. That is what you want when you are changing both sides. But then you have to start it too, with its whole tail of dependencies behind it.
- At the shared development environment. It saves you starting it, in exchange for VPN, for data that changes under your feet, and for the fact that if that environment is down, so are you.
- At a mock. Perfect when that service’s response is irrelevant to you, or when you cannot even reach it. In exchange for maintaining the stub.
The expensive part is not choosing: it is applying the choice. Each option means changing a URL in the configuration of a repository that is not yours, remembering not to commit it, and undoing it when you want to try something else. Multiply by services and by times a day.
And there is a trap: the correct URL depends on where the caller runs. A native
service reaches a container through localhost:port; a container calling another
container, through the service name; and a container calling something native on your
machine needs the special host name. The same dependency, three different URLs depending
on how each end is running.
4. The data
Section titled “4. The data”Schema created, migrations run and some minimal data. It is usually solved with the application’s own migrations plus a seed script.
The rule worth not breaking: the local environment does not touch shared databases. A migration accidentally fired against the common development environment is a lost afternoon for the whole team.
The ways to build it
Section titled “The ways to build it”| Approach | Good for | Where it falls short |
|---|---|---|
docker-compose for everything | Infrastructure; a small, stable system | Building your service’s image on every change; no comfortable hot reload or debugger; communication is still hand-written configuration |
Scripts + LOCAL_SETUP.md | Getting started fast | It ages: nobody updates it and it fails differently on every machine |
| Testcontainers | Real integration tests | The environment is born and dies with the test; it is not a place to work |
| Local Kubernetes (Kind, minikube) | Resembling production | A lot of machine and a lot of build cycle for the day to day; the change loop gets longer |
| Local orchestrator | The daily loop with several services | Each service has to be described once |
None of them is silly: they solve different things and often coexist. What none of the first four does is decide, per dependency, where each service calls, and apply it without touching the repositories.
How Aseptic solves it
Section titled “How Aseptic solves it”Aseptic is a desktop orchestrator that takes care of the four pieces, and treats the third as the main problem it is.
- You describe each service once. You point at the repository folder and its technology, its ports, the infrastructure it needs and who it calls get autodetected. You review it and that is that.
- You group into a scenario the services of a flow —“Checkout”, “Customer onboarding”— and they start together, in order and waiting for each one to be healthy.
- For each dependency you choose local, cloud or mock, and the engine
rewrites the URL on start by injecting it as
configuration —
-Dflags or environment variables—, with the right perspective depending on where each end runs. Your repositories are not touched. - The shared infrastructure (Kafka, Redis, PostgreSQL) is managed by the app in Docker, with automatic port remapping when they collide.
- Each service, native or containerised, as you choose: native the one you are working on, containerised the background ones.
And what you build gets shared: a scenario exports to a self-contained file, so whoever
joins clones, imports and starts instead of reading the LOCAL_SETUP.md.
Getting started
Section titled “Getting started”- Install Aseptic — it is free and all you need is Docker.
- Your first scenario in 5 minutes — the concrete steps.
- Mocking a dependency — if what is blocking you today is the VPN.