I have a rule: if I can't run the full stack on my laptop with docker compose up, the architecture is too complicated.
But then you need to deploy to production, and suddenly you're rewriting everything as Kubernetes manifests. The Compose file that worked on your machine is useless. Config lives in two places and they drift apart.
Here's the workflow I settled on after trying a bunch of things that didn't work well.
The Local Stack
Standard web app — API, Redis, Postgres. Three services.
# docker-compose.yml
services:
api:
build: .
ports:
- "8080:8080"
environment:
- DATABASE_URL=postgres://app:secret@db:5432/myapp
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 10s
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "app"]
cache:
image: redis:7-alpine
volumes:
pgdata:
docker compose up and the full stack is running. No external dependencies, fast iteration.
What I Tried and Abandoned
Kompose — kompose convert technically works but the output is ugly. Tons of annotations, weird formatting, needs so much cleanup I might as well write the YAML by hand.
Docker Compose on Kubernetes — Various tools that try to run Compose files directly on K8s. They all add complexity and break in subtle ways.
Trying to share one config — I wasted a weekend trying to make the same file work for both. Local dev and production have genuinely different requirements. Pretending otherwise creates worse problems.
What Actually Works: Convention Over Tooling
I keep two config sets, aligned by convention:
project/
├── docker-compose.yml # Local dev
├── Dockerfile
├── k8s/
│ ├── base/
│ │ ├── deployment.yaml
│ │ ├── service.yaml
│ │ └── kustomization.yaml
│ └── overlays/
│ ├── staging/
│ └── production/
└── Makefile
Same image names, same env var names, same port numbers in both places. When I change a port in Compose, I grep for it in k8s/ and update it. Manual, but nothing breaks silently.
The key differences between local and OKE:
- Database — Container locally, OCI managed service in production. I don't run databases on K8s.
- Secrets — Plain text in Compose, OCI Vault via External Secrets Operator on OKE.
- Scaling — One replica locally, HPA on OKE.
The K8s Side
# k8s/base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: iad.ocir.io/mytenancy/myapp:latest
ports:
- containerPort: 8080
envFrom:
- secretRef:
name: app-secrets
readinessProbe:
httpGet:
path: /health
port: 8080
periodSeconds: 10
Kustomize overlays handle the per-environment differences:
# k8s/overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: production
resources:
- ../../base
images:
- name: iad.ocir.io/mytenancy/myapp
newTag: v1.2.3
The Alignment Check I Actually Use
# Makefile
dev:
docker compose up --build
build:
docker build -t iad.ocir.io/$(TENANCY)/myapp:$(TAG) .
docker push iad.ocir.io/$(TENANCY)/myapp:$(TAG)
deploy-staging:
kubectl apply -k k8s/overlays/staging
check-alignment:
@echo "=== Compose ports ===" && grep -A1 "ports:" docker-compose.yml
@echo "=== K8s ports ===" && grep "containerPort" k8s/base/deployment.yaml
@echo "=== Compose health ===" && grep "test:" docker-compose.yml
@echo "=== K8s health ===" && grep "path:" k8s/base/deployment.yaml
make check-alignment is dumb but it catches drift. I run it before every deploy. It's saved me twice already from deploying with mismatched health check paths.
The Honest Take
This isn't elegant. I'd love a single config file that works everywhere. But every tool I tried to achieve that added more complexity than it removed.
The current setup is boring and it works. Compose for local, Kustomize for OKE, same Docker image, same env var names, a Makefile to keep me honest. I understand every piece of it, and when something breaks at 2am, that matters more than elegance.
Pavan Madduri — Oracle ACE Associate, CNCF Golden Kubestronaut. GitHub | LinkedIn | Website | Google Scholar | ResearchGate
Top comments (0)