Compose to Kubernetes Converter
Convert a compose file into Kubernetes manifests with Kompose-compatible semantics — entirely on this device.
Per-object mirrors kompose's one-file-per-object naming; the single stream joins the same objects with --- separators for one kubectl apply.
Kompose-compatible output — this site is not affiliated with or endorsed by the Kompose project. kompose.cmd / kompose.version annotations are omitted — this page is Kompose-compatible, it does not run Kompose. Your compose file never leaves this device.
Press ? for shortcuts
Do it offline:$ kompose convert -f docker-compose.yaml
How to use it
- Paste a docker-compose.yaml, or drop the file onto the input pane. Conversion runs as you type; files up to 256 KiB stay live.
- Read the object cards on the output side — each one is a complete Kubernetes manifest with a kompose-style file name such as web-deployment.yaml or webdata-persistentvolumeclaim.yaml.
- Check the notices under the output: portless services that get no Service object, depends_on entries that convert to nothing, and compose keys outside the converted subset are each named there.
- Copy or download per card, switch to the single-stream view for one kubectl apply -f, or take every file at once with Download all (.zip).
A compose file names services; a cluster wants Deployments, Services and PersistentVolumeClaims. Paste docker-compose.yaml and read the manifests back as one card per object, named the way the kompose CLI names its files, with a notice for every unconverted key. Kompose-compatible output — this site is not affiliated with or endorsed by the Kompose project. Nothing you paste leaves this device.
How it works
The restart policy decides the controller. A service with restart: always — or no restart key at all — becomes a Deployment whose pod template carries restartPolicy: Always. restart: "no" and restart: on-failure become bare Pods with restartPolicy: Never or OnFailure, because a Deployment exists to keep pods running and a run-to-completion container contradicts it. restart: unless-stopped has no cluster equivalent and is treated as always, with a notice saying so.
Volumes become PersistentVolumeClaims. A named volume turns into a claim carrying the volume's own name, mounted through a matching volumes: entry in the pod spec. An anonymous volume such as - /var/lib/postgresql/data gets a generated claim named <service>-claim0, counting up per service. A host bind mount keeps only its container side — the host path is ignored, a claim stands in, and a notice records the substitution. Every claim requests 100Mi and ReadWriteOnce (ReadOnlyMany for :ro mounts), and any service that mounts a claim gets strategy: type: Recreate, since two pods cannot share it during a rolling update.
Resource limits translate into container resources. cpus: "0.5" becomes cpu: 500m; deploy.resources.reservations become requests; and memory is printed as a quoted byte count — 256M becomes memory: "268435456", and mem_limit: 512m becomes a limits.memory of "536870912". The quotes are deliberate: the value is a string in the emitted YAML, exactly as the kompose CLI prints it.
Compose labels ride along as annotations in three places — the controller's metadata, its pod template, and the Service — while every object is selected through one generated io.kompose.service label. Ports map twice: the container lists each target port, and published ports become Service entries whose name is the published port number as a string.
Compatibility is measured, not assumed: the engine is verified byte-for-byte against a frozen corpus of twenty Kubernetes objects produced by kompose v1.38.0 across six compose fixtures — key order, indentation and quoting included. The one deliberate difference: kompose stamps kompose.cmd and kompose.version annotations recording its own invocation, and this page omits them, because it does not run Kompose.
Examples
services:
web:
image: nginx:1.27-alpine
ports:
- "8080:80"
environment:
- NGINX_HOST=example.com
- NGINX_PORT=80
volumes:
- webdata:/usr/share/nginx/html
volumes:
webdata: {}
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
io.kompose.service: web
name: web
spec:
replicas: 1
selector:
matchLabels:
io.kompose.service: web
strategy:
type: Recreate
template:
metadata:
labels:
io.kompose.service: web
spec:
containers:
- env:
- name: NGINX_HOST
value: example.com
- name: NGINX_PORT
value: "80"
image: nginx:1.27-alpine
name: web
ports:
- containerPort: 80
protocol: TCP
volumeMounts:
- mountPath: /usr/share/nginx/html
name: webdata
restartPolicy: Always
volumes:
- name: webdata
persistentVolumeClaim:
claimName: webdata
One web service fans out to three files: this Deployment, a web-service.yaml for the published 8080, and a webdata-persistentvolumeclaim.yaml. The named volume forces strategy: Recreate, and the numeric NGINX_PORT value is quoted so it stays a string.
services:
task-once:
image: busybox:1.36
command: ["sh", "-c", "echo done"]
restart: "no"
task-retry:
image: busybox:1.36
command: ["sh", "-c", "echo retry"]
restart: on-failure
web-always:
image: nginx:1.27-alpine
ports:
- "8090:80"
restart: always
apiVersion: v1
kind: Pod
metadata:
labels:
io.kompose.service: task-once
name: task-once
spec:
containers:
- args:
- sh
- -c
- echo done
image: busybox:1.36
name: task-once
restartPolicy: Never
The restart split in action: restart: "no" makes this a bare Pod with restartPolicy: Never, task-retry becomes a Pod with OnFailure, and only web-always earns a Deployment. The compose command lands in args because the image's own entrypoint stays in charge.
services:
api:
image: node:22-alpine
command: ["node", "server.js"]
ports:
- "3000:3000"
deploy:
resources:
limits:
cpus: "0.5"
memory: 256M
reservations:
cpus: "0.25"
memory: 128M
worker:
image: python:3.12-alpine
command: ["python", "worker.py"]
mem_limit: 512m
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
io.kompose.service: api
name: api
spec:
replicas: 1
selector:
matchLabels:
io.kompose.service: api
template:
metadata:
labels:
io.kompose.service: api
spec:
containers:
- args:
- node
- server.js
image: node:22-alpine
name: api
ports:
- containerPort: 3000
protocol: TCP
resources:
limits:
cpu: 500m
memory: "268435456"
requests:
cpu: 250m
memory: "134217728"
restartPolicy: Always
The parity-critical corner: memory arrives as a quoted byte count — 256M is "268435456", the 128M reservation becomes a request of "134217728" — and cpus: "0.5" is rewritten to the millicore form 500m. The portless worker gets a Deployment but no Service, and a notice says exactly that.
services:
db:
image: postgres:16.3-alpine
environment:
- POSTGRES_PASSWORD=localdev
volumes:
- /var/lib/postgresql/data
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
labels:
io.kompose.service: db-claim0
name: db-claim0
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Mi
An anonymous volume has no name to reuse, so the claim is generated as db-claim0 — a second one would be db-claim1 — and the Deployment mounts it under that name. The 100Mi request is the calibrated default; size it for real data before applying.
services:
frontend:
image: nginx:1.27-alpine
ports:
- "8082:80"
depends_on:
- backend
backend:
image: node:22-alpine
command: ["node", "api.js"]
ports:
- "3000:3000"
environment:
- DB_HOST=db
depends_on:
- db
db:
image: postgres:16.3-alpine
environment:
- POSTGRES_PASSWORD=localdev
volumes:
- dbdata:/var/lib/postgresql/data
volumes:
dbdata: {}
apiVersion: v1
kind: Service
metadata:
labels:
io.kompose.service: backend
name: backend
spec:
ports:
- name: "3000"
port: 3000
targetPort: 3000
selector:
io.kompose.service: backend
Three services become six files, and DB_HOST=db keeps working because each Service name doubles as cluster DNS. Both depends_on chains convert to nothing — startup order is a scheduler concern in Kubernetes — and the notice under the output is the only trace.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
A service without ports gets no Service object
Kubernetes Services exist to route traffic, so a compose service that publishes nothing produces only its controller and claims. The notice mirrors the CLI's own warning wording, naming the service, so a missing file is a stated decision rather than a silent one.
depends_on converts to nothing, loudly
The kompose CLI drops depends_on without a word; this page emits the same bytes but adds a notice, because an invisible deletion is the kind you ship. If ordering matters in the cluster, reach for readiness probes or an init container instead.
Memory is a quoted byte-count string
A limit of 256M prints as memory: "268435456" — mebibytes multiplied out to bytes and emitted as a string, matching the calibration corpus byte-for-byte. kubectl accepts the plain number form; rewrite it to 256Mi by hand if you prefer the human-readable unit, the two mean the same quantity.
The provenance annotations are deliberately absent
Native kompose stamps every object with kompose.cmd and kompose.version annotations recording how it was invoked. This page prints neither — claiming a CLI invocation that never happened would be a lie in your manifests. That omission is the only byte-level difference from the calibration corpus.
Host bind mounts lose their host half
A mount like ./html:/usr/share/nginx/html cannot follow you to a cluster node, so the host path is ignored and a generated <service>-claim0 PersistentVolumeClaim takes its place, exactly as the CLI does. The notice names the dropped path; put the content into an image or a real volume before deploying.
unless-stopped is treated as always
Kubernetes has no between state for restart policies, so restart: unless-stopped converts as a Deployment with restartPolicy: Always and a notice records the substitution — the same reading the CLI applies.
Keys outside the converted subset surface as notices
expose, network definitions, network_mode, env_file, the service-level tmpfs: key, deploy.replicas and other keys beyond the calibrated subset do not silently shape the output — each is named in a notice while the emitted objects stay untouched. (Long-syntax tmpfs volume mounts are inside the subset: they convert to Memory-backed emptyDirs.) env_file in particular cannot be read here: this page has no access to files sitting next to your compose file.
Common errors
Flow sequence in block collection must be sufficiently indented and end with a ] at line 4, column 1:- YAML syntax errors stop the conversion before any object is emitted — this one is an unclosed [ from a half-pasted list. The message keeps the parser's own line and column, and Jump to error puts the cursor there.
No services found — a compose file declares each container under a top-level services: mapping.- A file holding only volumes:, networks:, or a bare fragment has nothing to convert. Nest each container under services: — the top-level key is the whole grammar this converter reads.
Service "web" is not a mapping — each entry under services: must be a mapping of configuration keys.- services:\n web: nginx names a scalar where a mapping belongs. Compose wants the image on its own line — web: then image: nginx beneath it — and the converter refuses rather than guessing what the scalar meant.
This compose file expands to 81 Kubernetes objects — the cap for this tool is 80 objects. Nothing was emitted; split the file and convert it in parts.- Every service can fan out to a controller, a Service and several claims, so a large file multiplies quickly. The refusal names the real count against the 80-object cap and emits nothing, keeping the all-or-none guarantee.
Input is larger than the 256 KiB limit for this tool. For bigger files use the CLI: kompose convert -f docker-compose.yaml- The cap refuses outright instead of truncating, so a conversion always covers your whole file. 256 KiB outruns any hand-written compose file; past it, the same conversion belongs to the native CLI on your machine.
FAQ
Is this the same as running kompose convert?
It is calibrated to be: the engine is tested byte-for-byte against twenty objects that kompose v1.38.0 produced from six compose fixtures, including key order and quoting. The honest differences: the kompose.cmd and kompose.version annotations are omitted because no CLI ran; keys outside the calibrated subset raise notices instead of converting; and names that are not valid DNS-1123 labels are sanitized the same way in every emitted object, where the native tool can mix raw and sanitized forms. This site is not affiliated with the Kompose project.
Is my compose file uploaded anywhere?
No. Parsing and conversion run in your browser tab; the page makes no network request with your input, which matters because compose files routinely carry credentials in environment: lines. Credential-shaped input is also excluded from between-visit storage on this device.
Why did my service become a Pod instead of a Deployment?
Its restart policy asked for that: restart: "no" and restart: on-failure describe run-to-completion containers, so they emit bare Pods with restartPolicy: Never or OnFailure. A Deployment would immediately restart the pod it manages, which is the opposite of what those policies mean. Use restart: always — or drop the key — to get a Deployment.
Where did depends_on go?
Into a notice, and nowhere else. Kubernetes has no manifest key for startup ordering between workloads, and kompose drops depends_on silently; this page emits identical bytes but tells you it happened. Readiness probes, init containers, or retry logic in the dependent service are the cluster-native answers.
Why is memory printed as a quoted number?
Because that is the calibrated output: the CLI multiplies 256M out to bytes and prints memory: "268435456" as a YAML string, and this engine matches it exactly rather than editorializing. Kubernetes parses the quoted byte count correctly; converting it to 256Mi by hand is cosmetic.
Can I get a Helm chart instead of raw manifests?
Not on this page — v1 emits plain Kubernetes objects only. The native CLI has a chart mode, and if enough demand shows up a chart-shaped output may become an explicit option here; it will never replace the raw manifests silently.
Which compose keys does the converter understand?
The calibrated subset: image, build (image fallback only), ports, environment, volumes, labels, restart, deploy.resources, mem_limit, command, entrypoint, user, working_dir, healthcheck, container_name, tmpfs volumes in the long syntax (they become Memory-backed emptyDirs), profiles and depends_on. Everything else — expose, networks, env_file, deploy.replicas, the service-level tmpfs: key and the long tail — surfaces as a named notice while the emitted objects stay unchanged.
Keyboard shortcuts
- ⌘/Ctrl + Enter
- Re-run the conversion immediately.
- ⌘/Ctrl + S
- Download the Kubernetes YAML, overriding the browser's save dialog.
- ⌘/Ctrl + C
- Copy the whole output while no text is selected.
- ⌘/Ctrl + Shift + C
- Copy the whole output even mid-selection.
- t
- Advance the theme.
- ?
- Open the shortcut help overlay.
- Esc
- Dismiss the topmost overlay.