
There was a time, not so long ago, when docker-compose up felt like performing a magic trick. You’d scribble a few arcane incantations into a YAML file and, poof, your entire development stack would spring to life. The database, the cache, your API, the frontend… all humming along obediently on localhost. Docker Compose wasn’t just a tool; it was the trusty Swiss Army knife in every developer’s pocket, the reliable friend who always had your back.
Until it didn’t.
Our breakup wasn’t a single, dramatic event. It was a slow fade, the kind of awkward drifting apart that happens when one friend grows and the other… well, the other is perfectly happy staying exactly where they are. It began with small annoyances, then grew into full-blown arguments. We eventually realized we were spending more time trying to fix our relationship with YAML than actually building things.
So, with a heavy heart and a sigh of relief, we finally said goodbye.
The cracks begin to show
As our team and infrastructure matured, our reliable friend started showing some deeply annoying habits. The magic tricks became frustratingly predictable failures.
- Our services started giving each other the silent treatment. The networking between containers became as fragile and unpredictable as a Wi-Fi connection on a cross-country train. One moment they were chatting happily, the next they wouldn’t be caught dead in the same virtual network.
- It was worse at keeping secrets than a gossip columnist. The lack of native, secure secret handling was, to put it mildly, a joke. We were practically writing passwords on sticky notes and hoping for the best.
- It developed a severe case of multiple personality disorder. The same docker-compose.yml file would behave like a well-mannered gentleman on one developer’s machine, a rebellious teenager in staging, and a complete, raving lunatic in production. Consistency was not its strong suit.
- The phrase “It works on my machine” became a ritualistic chant. We’d repeat it, hoping to appease the demo gods, but they are a fickle bunch and rarely listened. We needed reliability, not superstition.
We had to face the truth. Our old friend just couldn’t keep up.
Moving on to greener pastures
The final straw was the realization that we had become full-time YAML therapists. It was time to stop fixing and start building again. We didn’t just dump Compose; we replaced it, piece by piece, with tools that were actually designed for the world we live in now.
For real infrastructure, we chose real code
For our production and staging environments, we needed a serious, long-term commitment. We found it in the AWS Cloud Development Kit (CDK). Instead of vaguely describing our needs in YAML and hoping for the best, we started declaring our infrastructure with the full power and grace of TypeScript.
We went from a hopeful plea like this:
# docker-compose.yml
services:
api:
build: .
ports:
- "8080:8080"
depends_on:
- database
database:
image: "postgres:14-alpine"
To a confident, explicit declaration like this:
// lib/api-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ecs_patterns from 'aws-cdk-lib/aws-ecs-patterns';
// ... inside your Stack class
const vpc = /* your existing VPC */;
const cluster = new ecs.Cluster(this, 'ApiCluster', { vpc });
// Create a load-balanced Fargate service and make it public
new ecs_patterns.ApplicationLoadBalancedFargateService(this, 'ApiService', {
cluster: cluster,
cpu: 256,
memoryLimitMiB: 512,
desiredCount: 2, // Let's have some redundancy
taskImageOptions: {
image: ecs.ContainerImage.fromRegistry("your-org/your-awesome-api"),
containerPort: 8080,
},
publicLoadBalancer: true,
});
It’s reusable, it’s testable, and it’s cloud-native by default. No more crossed fingers.
For local development, we found a better roommate
Onboarding new developers had become a nightmare of outdated README files and environment-specific quirks. For local development, we needed something that just worked, every time, on every machine. We found our perfect new roommate in Dev Containers.
Now, we ship a pre-configured development environment right inside the repository. A developer opens the project in VS Code, it spins up the container, and they’re ready to go.
Here’s the simple recipe in .devcontainer/devcontainer.json:
{
"name": "Node.js & PostgreSQL",
"dockerComposeFile": "docker-compose.yml", // Yes, we still use it here, but just for this!
"service": "app",
"workspaceFolder": "/workspace",
// Forward the ports you need
"forwardPorts": [3000, 5432],
// Run commands after the container is created
"postCreateCommand": "npm install",
// Add VS Code extensions
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
It’s fast, it’s reproducible, and our onboarding docs have been reduced to: “1. Install Docker. 2. Open in VS Code.”
To speak every Cloud language, we hired a translator
As our ambitions grew, we needed to manage resources across different cloud providers without learning a new dialect for each one. Crossplane became our universal translator. It lets us manage our infrastructure, whether it’s on AWS, GCP, or Azure, using the language we already speak fluently: the Kubernetes API.
Want a managed database in AWS? You don’t write Terraform. You write a Kubernetes manifest.
# rds-instance.yaml
apiVersion: database.aws.upbound.io/v1beta1
kind: RDSInstance
metadata:
name: my-production-db
spec:
forProvider:
region: eu-west-1
instanceClass: db.t3.small
masterUsername: admin
allocatedStorage: 20
engine: postgres
engineVersion: "14.5"
skipFinalSnapshot: true
# Reference to a secret for the password
masterPasswordSecretRef:
namespace: crossplane-system
name: my-db-password
key: password
providerConfigRef:
name: aws-provider-config
It’s declarative, auditable, and fits perfectly into a GitOps workflow.
For the creative grind, we got a better workflow
The constant cycle of code, build, push, deploy, test, repeat for our microservices was soul-crushing. Docker Compose never did this well. We needed something that could keep up with our creative flow. Skaffold gave us the instant gratification we craved.
One command, skaffold dev, and suddenly we had:
- Live code syncing to our development cluster.
- Automatic container rebuilds and redeployments when files change.
- A unified configuration for both development and production pipelines.
No more editing three different files and praying. Just code.
The slow fade was inevitable
Docker Compose was a fantastic tool for a simpler time. It was perfect when our team was small, our application was a monolith, and “production” was just a slightly more powerful laptop.
But the world of software development has moved on. We now live in an era of distributed systems, cloud-native architecture, and relentless automation. We didn’t just stop using Docker Compose. We outgrew it. And we replaced it with tools that weren’t just built for the present, but are ready for the future.