Docker Compose alternatives
Docker Compose is not a bad tool and most of the systems that outgrow it were never its intended shape. This page is about the point where it stops paying for itself, and about what the alternatives actually trade away — including ours, which is not the right answer for everyone.
When Compose is still the right answer
If your local environment is «a database, a queue and two services», Compose is a file you write once and forget. It is everywhere, everyone reads YAML, and no new tool needs to enter the team. Do not replace it out of fashion.
Where it starts to hurt
- Your code has to be an image. Every change means a rebuild, or a bind mount plus a reload trick per language, invented once per repository.
- It is all or nothing. Compose brings up what the file says. To run three services and point the rest at a shared environment, you edit the file or keep several of them.
- Debugging is a chore. A debugger against a container needs a port, a mapping and an IDE configuration that someone has to keep working.
- The file drifts. It lives in a repository, it describes other people's services, and it is nobody's job to update it.
- It knows nothing about health.
depends_onwaits for a container, not for a service that is ready to answer.
The same environment, both ways
This is the shape a docker-compose.yml ends up having for a small
system —two services and their infrastructure— once somebody has already fought the
change loop:
services:
postgres:
image: postgres:16
environment: { POSTGRES_PASSWORD: dev }
ports: ['5432:5432']
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 5s
kafka:
image: bitnami/kafka:3.7
environment:
KAFKA_CFG_NODE_ID: '0'
KAFKA_CFG_PROCESS_ROLES: controller,broker
KAFKA_CFG_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 0@kafka:9093
ports: ['9092:9092']
orders:
build: ../orders # rebuilt on every change
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/orders
PAYMENTS_URL: http://payments:8081
SPRING_KAFKA_BOOTSTRAP_SERVERS: kafka:9092
ports: ['8080:8080', '5005:5005'] # the second one is for the debugger
depends_on:
postgres: { condition: service_healthy }
payments:
build: ../payments
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/payments
ports: ['8081:8081']
It works. What it costs is not in the file, it is around it: every change to
orders goes through a build; port 5005 has to be agreed,
mapped and configured in everyone's IDE; PAYMENTS_URL is written by
hand, and pointing payments at the cloud means editing the file (and
remembering not to commit it); and this file lives in a repository that is not
payments' own, so nobody updates it when payments changes
a variable.
The Aseptic equivalent is not another file like this one: it is that the services
are not packaged at all. orders and payments start as
processes, with the hot reload their framework already provides and the debugger
attached as to any local process. The only thing still in Docker is the
infrastructure —Postgres and Kafka—, brought up by the app, which remaps their ports
if they are taken. And the URLs between services are not written down: they are
injected at startup, according to where you pointed each dependency.
orders ← local process, port 8080
├── postgres → infrastructure in Docker (Aseptic brings it up)
├── kafka → infrastructure in Docker
└── payments → local | cloud | mock ← this changes in one click
The line that matters is the last one. In Compose, «today I want
payments against the cloud environment» means editing YAML; here it
means changing one dependency's destination, and the rest of the scenario finds out
on its own. And none of it is stored inside your repositories: the scenario lives
outside, so there is no environment file to review in a pull request.
The alternatives, honestly
| Tool | What it really is | Cost of entry |
|---|---|---|
| Tilt | A fast inner loop against Kubernetes, with a UI showing builds, logs and health | A cluster, images and a Tiltfile per repo |
| Skaffold | Build, push and deploy to Kubernetes as one command, with profiles | A cluster, images, manifests and a skaffold.yaml |
| DevSpace | Your code running inside a development container in the cluster, with file sync | A cluster and comfort debugging on the other side of it |
| Garden | A dependency graph for build, deploy and test, shared by developers and CI | A platform decision, and someone to own the graph |
| Telepresence | One local process wired into a real cluster by intercepting its traffic | A healthy shared cluster, and connectivity to it |
| Local Kubernetes (kind, minikube) | The real primitives on your machine, so what you test is shaped like production | Memory, and the deploy cycle on every change |
| Aseptic | Services as native processes, each dependency pointed at local, cloud or a mock | Install it; Docker only for the shared infrastructure |
The pattern behind the list
Five of the seven answer «Compose is not enough» with «then use Kubernetes locally». That is a coherent answer, and if you deploy to Kubernetes it buys you parity that nothing else does. It also imports the cluster's build-and-deploy cycle into the loop you repeat two hundred times a day.
Aseptic is the odd one out on purpose: it assumes you do not want a cluster on your machine to test a flow. Your services run as processes, the shared infrastructure —Kafka, Redis, PostgreSQL— is brought up in Docker for you, and each dependency is resolved to a local service, to your shared cloud environment or to a mock, with the URLs rewritten at startup so no repository is touched.
Where Aseptic is the wrong choice
If what you need to test is the Kubernetes part —ingress, network policies, sidecars, probes, resource limits— Aseptic does not run any of it and cannot help. If your services only start inside a prepared namespace, running them outside is not a shortcut, it is a rewrite. And if daily proof that your images and manifests still work is worth more to you than a faster loop, Skaffold or Tilt give you that and Aseptic does not.
It is also in public beta, ships for Windows, macOS and Linux, and it is not open source. Every other tool on this list is open source, and for some teams that alone settles it.
How to choose without a spreadsheet
- Do your services start with environment variables and a database? If yes, running them natively is the cheapest loop you can have.
- Are you testing your code or your deployment? Your code does not need a cluster. Your deployment needs nothing else.
- Can one machine hold the system? If it genuinely cannot, Telepresence or a shared cluster is the honest answer.
- Who maintains the configuration? A tool whose config nobody owns will be abandoned within two quarters, whichever one you pick.
The wider comparison puts Docker Compose, a local Kubernetes and Aseptic side by side in more detail.
Frequently asked questions
What is the alternative to Docker Compose for microservices?
It depends on what is costing you. If it is production likeness, a local Kubernetes with Tilt or Skaffold; if it is the daily loop of working across several services, a local orchestrator. This page compares the five approaches and where each one breaks.
Does Docker Compose fall short for microservices?
Not for infrastructure, which is what it does best. It falls short once your services are inside the compose file: an image build per change, no comfortable hot reload, and the communication between them wired by hand.
Is there an option that does not need Kubernetes?
Yes: staying on Compose, or a local orchestrator like Aseptic, which uses Docker for infrastructure and runs your services as processes on the machine.