00 - Cloud-Native App Dev

How to go from Docker to Kubernetes the right way

Docker came a long way. It established itself very fast as the de-facto standard to get going with your container journey. It did not matter if you wanted to just play around with containers or if you wanted to build up new microservices, Docker was and still is a great choice to get yourself started. Docker was considered a great choice to run your apps in production as well, with the easy to learn, install and adapt Docker Swarm Orchestration.

Sadly, Docker never really embraced Kubernetes as the new (de-facto) Container Orchestration standard and tried everything to be its own thing. The last attempt was Docker Enterprise Edition which is now part of MIRANTIS. With the latest Kubernetes announcement to drop Docker Runtime support (NOT Docker Container Support!!!) it is even more clear now, that Docker’s glory days as Runtime of choice are ticking toward an end.

But let’s keep looking on the bright side of Docker. As said, Docker is great to get started to “play with containers and microservices”. But when your container runs and your docker-compose is up and running, how do you continue from there? How do you translate Docker, docker-compose, docker-stacks into a Kubernetes Deployment? Isn’t Kubernetes too overwhelming to “just use”?
This is what we want to have a look into in this article.

Let’s get started. Which microservice can we use to show off the Docker to Kubernetes story? Well, how about WordPress? Why WordPress? It is a very simple microservice (mysql-database, php-webserver), which delivers a good WYSIWYG experience and shows off interconnectivity of microservices in a straightforward way.

Please bear in mind, I want to keep the examples short, simple and understandable. Therefore I will refrain from too complicated orchestration examples, health checks, vast deployments, ingress routing and others. My config examples are neither best practice nor production environment recommendations! So please see this article just as a fun little kick-starter to your own journey.


Let’s get started with docker-compose. Just a real quick reminder: docker-compose let’s you run multiple containers at once, while you provide container images, local mount points and other configurations needed for your workload.

A Docker Compose can look like this [1]:

version: '3.3' services: db: image: mysql:5.7 volumes: - db_data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: Passw0rd!23 MYSQL_DATABASE: wordpress MYSQL_USER: wordpress MYSQL_PASSWORD: wordpress wordpress: depends_on: - db image: wordpress:latest ports: - "8000:80" restart: always environment: WORDPRESS_DB_HOST: db:3306 WORDPRESS_DB_USER: wordpress WORDPRESS_DB_PASSWORD: wordpress WORDPRESS_DB_NAME: wordpress volumes: db_data: {}
Code language: JavaScript (javascript)

To run the Docker Compose File you can either run

podman-compose -d up


docker-compose -d up

I will run the podman variant on my node called okd4-services, a CentOS Stream 8 box. After running the command I can access the service by browsing to my test box on the preconfigured port, don’t forget to configure your firewall when trying at home.

Now that our service is running, we should investigate the docker-compose YAML a bit further. We can find the following entries within a typical docker-compose YAML file:


This is the version of the compatible docker-compose interpreter. Depending on the version you can use features provided by docker-compose. If you are using docker-stack you will find the version field again for the same purpose. An overview of the versions can be found here [2].


Services might be the most important aspects of a docker-compose/stack file. In them you define your container images, provide information which storage or network they use, how they depend on other services, how health checks are handled, which network ports they will listen on and so much more. Basically here you define, what container is running and what additional information you feed into it.


Containers are by design ephemeral, that means data created by the container are lost when the container receives no information on where to save the data outside of itself. This is where volumes come in. Here you can define, that volumes are used or even mount external data, e.g. configuration files on your host into a container.

Docker Stack

While docker-compose is a fun way to see your first microservices in action, it is not very “production friendly”. In the prime days of Docker, the next step would have been to transform your compose file to a stack file. Docker-stacks files allowed basically the same instructions you gave your local Docker runtime to a Docker Swarm cluster. You will also see that they are very similar:

version: '3.3' services: db: image: mysql:5.7 volumes: - db_data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: Passw0rd!23 MYSQL_DATABASE: wordpress MYSQL_USER: wordpress MYSQL_PASSWORD: wordpress networks: - backend wordpress: depends_on: - db image: wordpress:latest volumes: - wordpress_data:/var/www/html/wp-content ports: - 8000:80 environment: WORDPRESS_DB_HOST: db:3306 WORDPRESS_DB_USER: wordpress WORDPRESS_DB_PASSWORD: wordpress WORDPRESS_DB_NAME: wordpress networks: - frontend - backend networks: frontend: backend: volumes: db_data: {} wordpress_data: {}
Code language: JavaScript (javascript)

You might notice that the stack YAML file is containing most of the same information as the compose file. Three major additions were added, which are:


There have been 2 networks added, frontend and backend. It is a typical security standard, that you do not expose your app directly to users and you do not want to expose your database at all. You would rather like a backend network, which interconnects your services, here WORDPRESS and MYSQL and you would like a frontend network, which exposes your app to your clients. You will also notice that the services now carry the network options, so you can connect them to the networks you want.

As a small side note: one best practice is putting a reverse-proxy service in front of your public facing service, such as NGINX, HAproxy or TRAEFIK. This will also be a fundamental part of Kubernetes managing its public facing services.


My example will store the database files the same way as compose, meaning on whichever node the MYSQL container will run, it will create a local storage volume. You DON’T want to do that in any kind of cluster, because that would render your database data inconsistent – you restart your container on another cluster node and your new data is basically created in a brand new database while your old data is stuck on the previous node. In a proper scenario you want to make sure to use shared storage which can be accessed from any node the same way. Examples here might be Cloud-S3 storage, NFS, CEPH, VMDK and others. Docker-Swarm allows you to do that and an NFS-example could look like that:

mysql-db: name: mysql-db driver: local driver_opts: type: nfs o: addr=,rw,local_lock=all,soft device: ":/mnt/db"
Code language: JavaScript (javascript)

With a Docker Swarm cluster in place, you can start your microservice with the following command: docker stack deploy -f stack-compose.yml WORDPRESS
By default Docker Swarm will share your defined public port on all nodes. I will deploy it on my test installation “ce-test01”.

Let’s sum up what we have learned so far:
We “created” our first microservice deployment and tested it locally or on a testbox with proper Firewall settings.
We moved our local microservice to a cluster and made sure our microservice is cluster ready (while neglecting the data access, since we are not using shared storage).
We made our app available by a predefined port and it can be accessed from any cluster node.


It’s time to step into gears and have a look into Kubernetes. But before we dive headfirst into deployments, services, ingress and more, let’s take a step back and do some translation work.

Yes, docker-compose and docker-stack are simple, while Kubernetes is more convoluted, BUT it is still manageable and very well documented by a lot of different communities.

So to translate our microservice into Kubernetes we will have a look into this list:

Pods are Containers

When managing containers within K8s, you will start to talk about pods. Pods are one to many containers. The idea behind PODs is sharing the resources multiple containers would share anyway, e.g memory, storage or network interconnection. So yes, you could simplify your container deployments but bundling them into one POD, but then you would miss out about the goodies K8s has to offer.

DOCKER Services are not Services in KUBERNETES!

Remember the YAML files we have seen with Docker? Our micro-services were named services: app-name: options: …, right? This is not the case in Kubernetes. Services is the piece of your microservice deployment, which will allow network connections to your applications. In other words, you define the ports your app is listening to within the Kubernetes cluster. To make it a bit more complicated, the services of Kubernetes are not externally available by default, so you define ports for INTERNAL kubernetes cluster communication. But let’s not open this can of worms yet, we will look into later.

Oh boy, so my STACK is also gone, what is there?

If you want to put it very simple, you could think of Kubernetes deployments as a replacement for your docker-stacks. Why? STACKs “told” your cluster where to run how many containers. Deployments (with the help of Replica-Sets) do basically the same. They tell the Kubernetes Cluster, how many pods should run where. STACKS connected containers with “internal networks” and storage. Deployments assign pods to namespaces (which allow also limited network access, among other Kubernetes network security features) and as well storage.
A big benefit of Kubernetes is that the deployment is loosely coupled to your services, ingress and storage configurations. That means you can start deploying your DEPLOYMENT while you can create and change the connecting components.

Storage, or better said Persistent Volumes

In Docker STACKs you simply mount your storage, may it be Local, NFS, SMB or other supported forms of storage. This basically keeps true within Kubernetes but instead of mounting it directly, Kubernetes introduces us to Persistent Volumes (PV) and Persistent Volume Claims (PVC). While PV take care of mounting the storage, PVC will handle the claims to it. Think of PVs as a bar-tender while you are the PVC when you order a beer. Wouldn’t it be great, if you could go to your bar and just say: I want something with 0,5% Alcohol in it and the bartender can offer you the right drink? This is what Kubernetes established with Storage Classes. You want 5GB of fast storage, gimme!

At this point, we looked at the important differences between the two orchestration services. Major points are:
How are containers treated and why are they PODs now? => (Pods)
How is a STACK handled now? => (Deployments and Replica Sets)
What is the very basic network service offering? => (Services)
How is Storage offered to my micro-services? => (Persistent Volumes)

We will go deeper on each of the topics so we can start the translation from docker-stack to Kubernetes deployments.

I will start to translate the service section of docker-stack:

services: db: image: mysql:5.7 volumes: - db_data:/var/lib/mysql environment: MYSQL_RANDOM_ROOT_PASSWORD: '1' MYSQL_DATABASE: wordpress MYSQL_USER: wordpress MYSQL_PASSWORD: wordpressPASS networks: - backend
Code language: JavaScript (javascript)

And this is how it will look like in K8s

apiVersion: apps/v1 kind: Deployment metadata: name: db labels: app: wordpress spec: selector: matchLabels: app: wordpress template: metadata: labels: app: wordpress spec: containers: - image: mysql:5.7 name: mysql env: - name: MYSQL_ROOT_PASSWORD value: Passw0rd!23 ports: - containerPort: 3306 name: mysql volumeMounts: - name: mysql-persistent-storage mountPath: /var/lib/mysql volumes: - name: mysql-persistent-storage persistentVolumeClaim: claimName: mysql-pv-claim
Code language: JavaScript (javascript)

Do not get spooked by the bigger fill of information within a Kubernetes YAML. It is basically the same information with a bit more content and possibilities.

  • So your Docker Service name can now be found in your metadata name entry. 
  • Instead of simply providing the image you define the image under containers. This allows you to provide multiple images (Pods can be multiple containers).
  • You still provide ENVIRONMENT VARS next to your image definitions.
  • Volumes became a bit more convoluted but with a lot of new features, e.g. Volume Claims. Basically you still define your mount points the same way as in Docker , first the mount point, than the volume itself.
  • Network Ports are still provided. They will define which ports of the Pod will be available for services. While in stack you could also provide the forwarded port which you could access over the network. At this point Kubernetes becomes again a bit more convoluted but not without good reason.

So as you can see, you will not have to start completely from scratch when working with Kubernetes. You could even deploy your “DEPLOYMENT” and your Pods while scale up and start doing their jobs, except if they are in need of volumes.

Next I will provide the complete translated Kubernetes cookbook YAMLs to deploy WORDPRESS with MYSQL on Kubernetes:


apiVersion: v1 kind: Service metadata: name: db labels: app: wordpress spec: ports: - port: 3306 selector: app: wordpress tier: mysql clusterIP: None --- kind: PersistentVolume apiVersion: v1 metadata: name: mysql-pv-volume labels: type: local spec: storageClassName: manual capacity: storage: 2Gi accessModes: - ReadWriteOnce hostPath: path: "/mnt/data" --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mysql-pv-claim labels: app: wordpress spec: storageClassName: manual accessModes: - ReadWriteOnce resources: requests: storage: 2Gi --- apiVersion: apps/v1 kind: Deployment metadata: name: wordpress-mysql labels: app: wordpress spec: selector: matchLabels: app: wordpress tier: mysql strategy: type: Recreate template: metadata: labels: app: wordpress tier: mysql spec: containers: - image: mysql:5.7 name: mysql env: - name: MYSQL_ROOT_PASSWORD value: Passw0rd!23 - name: MYSQL_DATABASE value: wordpress - name: MYSQL_USER value: wordpress - name: MYSQL_PASSWORD value: wordpress ports: - containerPort: 3306 name: mysql volumeMounts: - name: mysql-persistent-storage mountPath: /var/lib/mysql volumes: - name: mysql-persistent-storage persistentVolumeClaim: claimName: mysql-pv-claim
Code language: JavaScript (javascript)


apiVersion: v1 kind: Service metadata: name: wordpress labels: app: wordpress spec: ports: - port: 80 selector: app: wordpress tier: frontend type: NodePort --- apiVersion: v1 kind: PersistentVolume metadata: name: wordpress-pv-volume labels: type: local spec: storageClassName: manual capacity: storage: 2Gi accessModes: - ReadWriteOnce hostPath: path: "/mnt/data" --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: wordpress-pv-claim labels: app: wordpress spec: storageClassName: manual accessModes: - ReadWriteOnce resources: requests: storage: 2Gi --- apiVersion: apps/v1 kind: Deployment metadata: name: wordpress labels: app: wordpress spec: selector: matchLabels: app: wordpress tier: frontend strategy: type: Recreate template: metadata: labels: app: wordpress tier: frontend spec: containers: - image: wordpress:latest name: wordpress env: - name: WORDPRESS_DB_HOST value: db:3306 - name: WORDPRESS_DB_USER value: wordpress - name: WORDPRESS_DB_PASSWORD value: wordpress - name: WORDPRESS_DB_NAME value: wordpress ports: - containerPort: 80 name: wordpress volumeMounts: - name: wordpress-persistent-storage mountPath: /var/www/html volumes: - name: wordpress-persistent-storage persistentVolumeClaim: claimName: wordpress-pv-claim
Code language: JavaScript (javascript)

The YAMLs consist both the following Kubernetes elements:

  • Service
  • Persistent Storage
  • Persistent Storage Claim
  • Deployment

Within Service I defined the MYSQL port and the Web Port for WORDPRESS. The WORDPRESS service has the important little extra of being a NodePort. That means the Service Port (80) will receive a randomly selected port in the predefined high Range of Kubernetes (something like 25123) which can be accessed from the outside. You can define a NodePort yourself, but NodePorts are limited to the Kubernetes predefined ranges.

Again you might think “Wow, why? That is so complicated!?” As soon as you roll out more services in a Kubernetes cluster and start to learn how an Ingress-Controller with Load Balancer Services work, you will love the result. Run as many workloads as you like and let Load Balancers deploy your IPs, reachable DNS entries and even deploy certificates automatically.

In Persistent Storage I defined a local storage with a fixed size. That means Kubernetes will reserve 2GB of the nodes local storage which can be used for my pod. Here we see another difference between Swarm and Kube. I can restart my pod without changing my storage configuration or I can update my volume mount point without touching the local volume configuration.

The Persistent Storage Claim will combine the deployment/pod with the provided storage. You will easily see the benefit of this, when you start to use Dynamic Volume Claims. There you can predefine storage offerings, e.g. VMDK, NFS, CEPH Storage and the Volume Claim will collect it’s share of needed storage.

I explained the deployment part in the beginning of this chapter, so now we can fire off our deployment.

kubectl apply -f mysql-deployment.yml -n wordpress kubectl apply -f wordpress-deployment.yml -n wordpress
Code language: CSS (css)

When the pods are up and running, we need to check the randomized port of the NodePort to access our service via a browser. This can be done with kubectl get services -n wordpress

Instead of using the Docker command, we will be starting to use kubectl. My previous command is pretty straight forward, but what is that -n wordpress? That is one great Kubernetes addition. Kubernetes is working with NAMESPACES. You could compare them to VLANs within a network. They are logical separations of the Kubernetes Cluster. I could go the extra mile and create a NAMESPACE for WORDPRESS and another one for MYSQL, so they would be separated. And what would that do me any good? I could allow ONLY SQL-Admins to deploy MYSQL pods into the predefined namespaces and allow Web-Developers only access to WORDPRESS. Neat, isn’t it?


Of course this article will not make a Kubernetes expert out of you and Kubernetes IS a behemoth to be reckoned with, but there is one more thing you should have a look into. 

YAMLs are great and all, but how about some simplifications? Wouldn’t it be great if you could start every YAML for Kubernetes with a guide? Wouldn’t it be great to have a UI to work with in Kubernetes? Wouldn’t it be sweet if you could forget about YAMLs for a bit all together? YES? Ok, then let’s have a look into Red Hat OpenShift (OCP)!

Again, my goal will be to have a running MYSQL database with a wordpress in front of it. So I logged into my OpenShift deployment. With a couple of simple clicks I created my first project “Wordpress”, which is basically a namespace we used in Kubernetes. Then I switch to my developer view and I can get started.

How should I provide my database? With the Database Catalog!

I simply select the Database option and I will receive a nice selection of Database providers. I will go ahead with the MySQL Provider.

I will see a template where I can fill in most of the details I used for my previous deployments as well, such as Database name, root Password and more.

I click create and the POD is rolling in… oh but wait! The image here is based on MySQL 8, oh that’s no good to me, because there were some significant changes since 5.7 and well, I’m not really up to date I’m afraid. So what can I do?

Easy! I can choose deployment option Number 2. I will deploy my database from the same Image I used before.

I go back by clicking add, but this time I choose the Container Image option.

Again I receive a template to fill in all my details, such as the IMAGE source, environment variables and more.

I click create and NOW I have my database as I need it in front of me. Great!

Next and last step should be my WORDPRESS container right? I would like to use the same image again, but I would rather build a new one directly from a GIT. I believe this is content for a complete new article? How about no. 

OpenShift has a third option I would like to use here. Import from GIT, which is also known as S2I / Source to Image. So again I go back and choose that option.

This time I enter the official WORDPRESS GIT, select the PHP Builder Image (as it is a PHP based web application). I stick with the latest version as I want the latest bug fixes being included. And YET AGAIN I can create all the details I need in my environment variables.

Now here comes something special. Do you remember how I tried to explain the Kubernetes Network infrastructure, with services, ingress controller, load balancer? What would you say, if you would be able to reach your homepage with a single click of an option? You can do that here! Simply select Create a route to the application and OpenShift will take care of it. You can later check the route and access your page with a simple click of a button.

Ok, my applications are up and running and I can see them in a nice graphical overview without any command hassles.

I can even monitor my workloads by simply clicking on the different services. I can see how much my database is in need of CPU or memory for instance:

Now let’s click on that little route icon on the top right corner of my PHP icon and see where it leads me to…

There we go.

Within the Screenshot you can see the URL. This URL was automatically generated by OpenShift. I can change it to an official or more Human friendly URL any time. But for a quick start that’s ok and did help to speed up my deployment.


Transitioning from docker-compose or docker-swarm to Kubernetes Deployments is not easy, but within the last couple of years, so many documentations, tools and community work has made it so much more fun. If you do not want to lay hands on any of your compose files, but you rather want some tool to transform your files to Kubernetes YAMLS you should have a look at KOMPOSE

You might also raise a brow and ask: “I’m just getting started and you are showing me a commercially used solution? I want to start with containers and not buying products to GET started”. That is no problem as well! You can get started with OKD, the upstream project of Red Hat OpenShift. With OKD you can use the same feature set as in OpenShift. If you need to shift from testing/developing your skills to production, you can always come back to OpenShift AND use the same skills you have learned with OKD.

And those are just 2 very small examples of where you could start your journey with. The easiness of a SWARM cluster will be very missed in the future, but the Kubernetes future’s so bright and far from being done anytime soon.

So let’s get translating and set sail with your new orchestration in command, Kubernetes (which is Greek and basically means helmsman or pilot so… yeah, that’s that 🙂 )

The following sources were used to shape this articel:





By Stefan Trimborn

I call myself at "" within IT for over "1010" years. Since I live near Frankfurt am Main, I enjoyed a kickstart in the Finance-IT field, joined Telecommunication and Industrial-IT Teams and later joined as technical sales role different global players, such as Trend Micro or Docker. Now I call my self at "/home" at Red Hat and enjoy to share my knowledge as well as learn so much from you and my peers. And always remember, ACTIONS speak louder than PowerPoint slides do ;-)

One reply on “How to go from Docker to Kubernetes the right way”