Tools: From Docker Compose on My Laptop to OKE in Production — Same App, Zero Rewrites (2026)

Tools: From Docker Compose on My Laptop to OKE in Production — Same App, Zero Rewrites (2026)

The Local Stack

What I Tried and Abandoned

What Actually Works: Convention Over Tooling

The K8s Side

The Alignment Check I Actually Use

The Honest Take 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. Standard web app — API, Redis, Postgres. Three services. docker compose up and the full stack is running. No external dependencies, fast iteration. 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. I keep two config sets, aligned by convention: 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: Kustomize overlays handle the per-environment differences: 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. 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 Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

# -weight: 500;">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", "-weight: 500;">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: # -weight: 500;">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", "-weight: 500;">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: # -weight: 500;">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", "-weight: 500;">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: project/ ├── -weight: 500;">docker-compose.yml # Local dev ├── Dockerfile ├── k8s/ │ ├── base/ │ │ ├── deployment.yaml │ │ ├── -weight: 500;">service.yaml │ │ └── kustomization.yaml │ └── overlays/ │ ├── staging/ │ └── production/ └── Makefile project/ ├── -weight: 500;">docker-compose.yml # Local dev ├── Dockerfile ├── k8s/ │ ├── base/ │ │ ├── deployment.yaml │ │ ├── -weight: 500;">service.yaml │ │ └── kustomization.yaml │ └── overlays/ │ ├── staging/ │ └── production/ └── Makefile project/ ├── -weight: 500;">docker-compose.yml # Local dev ├── Dockerfile ├── k8s/ │ ├── base/ │ │ ├── deployment.yaml │ │ ├── -weight: 500;">service.yaml │ │ └── kustomization.yaml │ └── overlays/ │ ├── staging/ │ └── production/ └── Makefile # 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 # 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 # 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 # 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 # 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 # 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 # Makefile dev: -weight: 500;">docker compose up --build build: -weight: 500;">docker build -t iad.ocir.io/$(TENANCY)/myapp:$(TAG) . -weight: 500;">docker push iad.ocir.io/$(TENANCY)/myapp:$(TAG) deploy-staging: -weight: 500;">kubectl apply -k k8s/overlays/staging check-alignment: @echo "=== Compose ports ===" && grep -A1 "ports:" -weight: 500;">docker-compose.yml @echo "=== K8s ports ===" && grep "containerPort" k8s/base/deployment.yaml @echo "=== Compose health ===" && grep "test:" -weight: 500;">docker-compose.yml @echo "=== K8s health ===" && grep "path:" k8s/base/deployment.yaml # Makefile dev: -weight: 500;">docker compose up --build build: -weight: 500;">docker build -t iad.ocir.io/$(TENANCY)/myapp:$(TAG) . -weight: 500;">docker push iad.ocir.io/$(TENANCY)/myapp:$(TAG) deploy-staging: -weight: 500;">kubectl apply -k k8s/overlays/staging check-alignment: @echo "=== Compose ports ===" && grep -A1 "ports:" -weight: 500;">docker-compose.yml @echo "=== K8s ports ===" && grep "containerPort" k8s/base/deployment.yaml @echo "=== Compose health ===" && grep "test:" -weight: 500;">docker-compose.yml @echo "=== K8s health ===" && grep "path:" k8s/base/deployment.yaml # Makefile dev: -weight: 500;">docker compose up --build build: -weight: 500;">docker build -t iad.ocir.io/$(TENANCY)/myapp:$(TAG) . -weight: 500;">docker push iad.ocir.io/$(TENANCY)/myapp:$(TAG) deploy-staging: -weight: 500;">kubectl apply -k k8s/overlays/staging check-alignment: @echo "=== Compose ports ===" && grep -A1 "ports:" -weight: 500;">docker-compose.yml @echo "=== K8s ports ===" && grep "containerPort" k8s/base/deployment.yaml @echo "=== Compose health ===" && grep "test:" -weight: 500;">docker-compose.yml @echo "=== K8s health ===" && grep "path:" k8s/base/deployment.yaml - Database — Container locally, OCI managed -weight: 500;">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.