Docker Compose to Kubernetes converter
Paste a Compose file and get Kubernetes manifests, with a gap report of everything that has no equivalent. Your file stays in this browser, and no secret value is copied.
Paste the text, or choose a file from your computer. Up to 512 KiB.
0 of 512 KiB
What it does
It reads a Compose file with the same safe parser as the Compose Analyzer and writes one kubernetes.yaml: a Deployment per service, or a StatefulSet for a database image with a volume of its own; a Service for the ports other pods connect to; a PersistentVolumeClaim per named volume; plain environment values in a ConfigMap and secret-looking ones in a Secret that holds placeholders only; a readiness probe from healthcheck; requests and limits from deploy.resources; and, if you ask, an Ingress. Everything else is in the gap report, with what to do instead.
How it works
- The file is parsed with size, depth and alias limits; no YAML tag or include is followed, and nothing is run or fetched.
- Each Compose key is either mapped to its Kubernetes field or reported. Bind mounts, the Docker socket, network_mode: host, privileged mode, build: and depends_on conditions have no safe equivalent: each one gets a gap with the alternative.
- Values that look like secrets (PASSWORD, TOKEN, *_KEY, CREDENTIALS) are never copied. Variables Compose reads from .env become keys of the compose-dotenv ConfigMap or Secret, and the containers read them from there; ${VAR} in a command becomes Kubernetes' own $(VAR).
- The manifests are validated in our tests against the Kubernetes API schema, and the output is deterministic: the same file always gives the same bytes.
Example
WordPress with MariaDB, the database password read from .env, converted:
services:
wordpress:
image: wordpress:6.8-apache
restart: unless-stopped
ports:
- "127.0.0.1:8080:80"
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: ${DB_PASSWORD:?Set it in .env}
WORDPRESS_DB_NAME: wordpress
volumes:
- wordpress:/var/www/html
depends_on:
db:
condition: service_healthy
db:
image: mariadb:11.8
restart: unless-stopped
environment:
MARIADB_DATABASE: wordpress
MARIADB_USER: wordpress
MARIADB_PASSWORD: ${DB_PASSWORD:?Set it in .env}
MARIADB_RANDOM_ROOT_PASSWORD: "1"
volumes:
- db:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 3
volumes:
wordpress: {}
db: {}
# Generated by the ANAROQ Compose to Kubernetes converter: https://anaroq.com/tools/compose-to-kubernetes
# Converter 1.0.0. Not everything in Compose has a Kubernetes equivalent: read the gap report before applying.
# Secrets hold the placeholder REPLACE_ME: set real values before `kubectl apply -f kubernetes.yaml`.
---
apiVersion: "v1"
kind: "ConfigMap"
metadata:
name: "db-env"
labels:
"app.kubernetes.io/name": "db"
data:
MARIADB_DATABASE: "wordpress"
MARIADB_USER: "wordpress"
MARIADB_RANDOM_ROOT_PASSWORD: "1"
---
apiVersion: "v1"
kind: "ConfigMap"
metadata:
name: "wordpress-env"
labels:
"app.kubernetes.io/name": "wordpress"
data:
WORDPRESS_DB_HOST: "db"
WORDPRESS_DB_USER: "wordpress"
WORDPRESS_DB_NAME: "wordpress"
---
apiVersion: "v1"
kind: "Secret"
metadata:
name: "compose-dotenv"
type: "Opaque"
stringData:
DB_PASSWORD: "REPLACE_ME"
---
apiVersion: "v1"
kind: "PersistentVolumeClaim"
metadata:
name: "wordpress"
spec:
accessModes:
- "ReadWriteOnce"
resources:
requests:
storage: "1Gi"
---
apiVersion: "v1"
kind: "Service"
metadata:
name: "db"
labels:
"app.kubernetes.io/name": "db"
spec:
clusterIP: "None"
selector:
"app.kubernetes.io/name": "db"
ports:
-
name: "tcp-3306"
port: 3306
targetPort: 3306
protocol: "TCP"
---
apiVersion: "v1"
kind: "Service"
metadata:
name: "wordpress"
labels:
"app.kubernetes.io/name": "wordpress"
spec:
selector:
"app.kubernetes.io/name": "wordpress"
ports:
-
name: "tcp-80"
port: 80
targetPort: 80
protocol: "TCP"
---
apiVersion: "apps/v1"
kind: "Deployment"
metadata:
name: "wordpress"
labels:
"app.kubernetes.io/name": "wordpress"
spec:
replicas: 1
selector:
matchLabels:
"app.kubernetes.io/name": "wordpress"
strategy:
type: "Recreate"
template:
metadata:
labels:
"app.kubernetes.io/name": "wordpress"
spec:
enableServiceLinks: false
containers:
-
name: "wordpress"
image: "wordpress:6.8-apache"
env:
-
name: "WORDPRESS_DB_PASSWORD"
valueFrom:
secretKeyRef:
name: "compose-dotenv"
key: "DB_PASSWORD"
envFrom:
-
configMapRef:
name: "wordpress-env"
ports:
-
name: "tcp-80"
containerPort: 80
protocol: "TCP"
volumeMounts:
-
name: "wordpress"
mountPath: "/var/www/html"
volumes:
-
name: "wordpress"
persistentVolumeClaim:
claimName: "wordpress"
---
apiVersion: "apps/v1"
kind: "StatefulSet"
metadata:
name: "db"
labels:
"app.kubernetes.io/name": "db"
spec:
serviceName: "db"
replicas: 1
selector:
matchLabels:
"app.kubernetes.io/name": "db"
template:
metadata:
labels:
"app.kubernetes.io/name": "db"
spec:
enableServiceLinks: false
containers:
-
name: "db"
image: "mariadb:11.8"
env:
-
name: "MARIADB_PASSWORD"
valueFrom:
secretKeyRef:
name: "compose-dotenv"
key: "DB_PASSWORD"
envFrom:
-
configMapRef:
name: "db-env"
ports:
-
name: "tcp-3306"
containerPort: 3306
protocol: "TCP"
readinessProbe:
exec:
command:
- "healthcheck.sh"
- "--connect"
- "--innodb_initialized"
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
volumeMounts:
-
name: "db"
mountPath: "/var/lib/mysql"
volumeClaimTemplates:
-
metadata:
name: "db"
spec:
accessModes:
- "ReadWriteOnce"
resources:
requests:
storage: "1Gi"
And its gap report:
- wordpress publishes 127.0.0.1:8080→80 on the Docker host. A ClusterIP Service is reachable inside the cluster only.
- wordpress waits for db to be service_healthy. Kubernetes starts every pod at once and does not wait.
- db declares no port; its Service uses 3306, the default port of the official mariadb image.
- Compose reads DB_PASSWORD from .env or the shell, and they look like secrets. The Secret compose-dotenv holds placeholders for them, never values.
- Compose volumes have no size. The claims for wordpress, db request 1Gi each.
Limits
- Static conversion: nothing is applied to a cluster, so images, ports and paths are not checked against a running system.
- Not a Helm chart and not an operator: one plain manifest file, with no templating, upgrades or backups.
- Volumes get a 1Gi claim each and the cluster's default storage class: set the sizes and the class your data needs.
- No liveness probe is added: Docker does not restart an unhealthy container, so a readiness probe is the faithful mapping.
- env_file and the files of bind mounts are not read: the converter only sees the Compose file you paste.
When not to move to Kubernetes
Kubernetes pays off when you run many services across several machines, need rolling updates without downtime, or already have a cluster and a team that operates it. For one server, Docker Compose is simpler to run, to back up and to debug, and the same file keeps working.
A cluster adds its own work: a control plane, storage classes, ingress, certificates, upgrades every few months. A single-node cluster gives you that work without the redundancy. If the reason to move is one more server, a second Compose host behind a reverse proxy is often enough.
Methodology: how the numbers are worked out
Sources
- Docker Docs: Compose file reference
- Compose Specification
- Kubernetes Documentation: Workloads
- Kubernetes Documentation: Configure Liveness, Readiness and Startup Probes
- Kubernetes Documentation: Define Dependent Environment Variables
- Kubernetes API schema used to validate the output (kubernetes-models 5.1.0)
Related
Next step
A free public tool by ANAROQ, a company building compute infrastructure. What ANAROQ is building