Docker and Kubernetes 101

Omar Wit,

Mission Critical Engineer @ Schuberg Philis


Andy Repton,

Mission Critical Engineer @ Schuberg Philis



  • Intro
  • What is a container?
  • Lab: Working with Images
  • What is Kubernetes?
  • Lab: Getting started with kubectl
  • Lab: Developing a Wordpress website in Kubernetes
  • Lab: Bringing your App to production

Important links for today


What is a container?

A container is a packaged unit of software

  • Put all of your code and dependencies together
  • Only include what you need
  • Isolate your code from the rest of the operating system
  • Share the host kernel

Docker - a current standard

What's the difference to a VM?

Immutable vs. Converging infrastructure

Why should you use them?

Example usecases of docker containers in day to day life

What questions do you have about them?

Lab: Working with images

Building a Docker Image

Building our image

  • Docker images are built using a Dockerfile
  • Make sure you're inside the workshop repository and create a new file named 'Dockerfile' using your favourite text editor
# If on Linux/Mac:
$ cd docker-and-kubernetes-101/lab01/src

$ vim Dockerfile

# If on Windows: 
$ Set-Location docker-and-kubernetes-101\lab01\src

#use notepad to create the file named 'Dockerfile'

Building our Dockerfile

# Base image from the default nginx image
FROM nginx:1.15-alpine

Inside the Dockerfile, we start with a FROM keyword. This allows us to pull in a preexisting container image and build on top of it

Here, we're going to start with the official nginx image (when the URL is not set Docker will default to the Docker Hub) and use the alpine tag of that image

# Docker format:


Building our Dockerfile

# Base image from the default Node 8 image
FROM nginx:1.15-alpine

# Set /usr/share/nginx/html as the directory where our site resides
WORKDIR /usr/share/nginx/html

Next, we'll set the WORKDIR for the container. This will be the where docker will run the process from

Every new command in the Dockerfile will create a new layer. Any layers that already exist on the host will be reused rather than recreated. Docker keeps track of these using a hash

Building our Dockerfile

# Base image from the default nginx
FROM nginx:1.15-alpine

# Set /usr/share/nginx/html as the directory where our app resides
WORKDIR /usr/share/nginx/html

# Copy the source of the website to the container
COPY site .

And now we can COPY our Website into the container

You cannot COPY from above the directory you're currently in

# Base image from the default nginx
FROM nginx:1.15-alpine

# Set /usr/share/nginx/html as the directory where our app resides
WORKDIR /usr/share/nginx/html

# Copy /etc/passwd
COPY /etc/passwd /usr/share/nginx/html

For example, this won't work:

docker build .
Sending build context to Docker daemon  230.4kB
Step 1/2 : FROM nginx:1.15-alpine
 ---> df48b68da02a
Step 2/2 : COPY /etc/passwd /usr/share/nginx/html
COPY failed: stat /var/lib/docker/tmp/docker-builder918813293/etc/passwd: no such file or directory

Building our Dockerfile

# Base image from the default nginx
FROM nginx:1.15-alpine

# Set /usr/share/nginx/html as the directory where our app resides
WORKDIR /usr/share/nginx/html

# Copy the source of the website to the container
COPY site .

#Exposing port 80

Next, we will EXPOSE the container on port 80, so we can reach the application

Tagging our image


$ docker build . -t organisation/<image-name>:<tag>

organisation: sbpdemo
image name  : bkwi-yourname

$ docker tag <image-id> mysuperwebserver:v1

1. During build

2. Afterwards (re-tagging)

Running our image locally

$ docker run --name my-website -d sbpdemo/bkwi-omar:latest do we view it?

Re-running with a port-forward

$ docker run --name my-website -p 8080:80 -d sbpdemo/bkwi-omar:latest

And now we can view it at http://localhost:8080

Why didn't that work? We need to stop the existing container first

Stopping, restarting and viewing our container

$ docker ps
docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS               NAMES
a1a144e99570        bkwi-omar:latest             "nginx -g 'daemon of…"   About a minute ago   Up About a minute   80/tcp              my-website

We can view our running containers with:

And stop it, before starting it again:

$ docker rm -f a1a144e99570

$ docker run --name my-website -p 8080:80 bkwi-omar:latest

And now we can actually view it!

Pushing our image to our registry

$ docker push sbpdemo/<my-image>:latest

Why didn't that work? Logging into the registry

Logging into a registry

$ docker login
Login with your Docker ID to push and pull images from Docker Hub. 
Username: sbpdemo
Password: *Pass*

What is Kubernetes?

Kubernetes Building Blocks

  • Pods
  • Replica Sets
  • Deployments
  • Services
  • Ingresses
  • Secrets
  • Config Maps
  • Daemon Sets
  • Stateful Sets
  • Namespaces


When we think of 'Bottom Up', we start with the node:


Then we add the container runtime


And then the containers


When we move from the 'physical' layer to kubernetes, the logical wrapper around containers is a pod


  • Pods are the basic building blocks of Kubernetes.
  • A pod is a collection of containers, storage and options dictating how the pod should run.
  • Pods are mortal, and are expected to die in order to rebalance the cluster or to upgrade them.
  • Each pod has a unique IP in the cluster, and can listen on any ports required for your application. 


Pods support Health checks and liveness checks. We’ll go through those later

Pod Spec

apiVersion: v1 # The APiVersion of Kubernetes to use
kind: Pod # What it is!
  name: pod # The name
  namespace: default # The namespace (covered later)
  - image: centos:7 # The image name, if the url/repo is not specified docker defaults to docker hub
    imagePullPolicy: IfNotPresent # When to pull a fresh copy of the image
    name: pod # The name again
    command: ["ping"] # What to run
    args: ["-c", "4", ""] # Args to pass to the container

Replica Sets

Replica Sets

  • Replica sets define the number of pods that should be running at any given time.
  • With a replica set, if a pod dies or is killed and the replica set no longer matches the required number, the scheduler will create a new pod and deploy it inside the cluster.
  • If too many pods are running, the replica set will delete a pod to bring it back to the correct number.
  • Replica sets can be adjusted on the fly, to scale up or down the number of pods running.
  • Editing a replica set has no impact on running pods, only new ones

Replica Set Spec

apiVersion: apps/v1 # New API here!
kind: ReplicaSet # What it is
  name: replica-set # Name of the replica set, *not* the pod
  labels: # Used for a bunch of things, such as the service finding the pods (still to come)
    app: testing
    awesome: true # Because we are
  replicas: 2 # How many copies of the pod spec below we want
    matchLabels: # Needs to be the same as below, this is how the replica set finds its pods
      awesome: true
  template: # This below is just pod spec! Everything valid there is valid here
        awesome: true
      - name: pod
        image: centos:7



  • A Deployment is a copy of a replica set and pods.
  • If a deployment changes, the cluster will automatically create a new replica set with the new version and will automatically live upgrade your pods.
  • A deployment can also be rolled back to a previous version if there is an error.

Deployment Spec

apiVersion: apps/v1 # There's that API again
kind: Deployment # Pretty self explanatory at this point
  name: pod-deployment
    app: pod
    awesome: true
spec: # Anyone recognise this? Yep, it's Replica Set Spec
  replicas: 2
      app: pod
        app: pod
    spec: # Aaaaand yep, this is pod spec
      - name: nginx
        image: nginx:1.7.9
        - containerPort: 80



  • Services expose your pods to other pods inside the cluster or to the outside world.
  • A service uses a combination of labels and selectors to match the pods that it’s responsible for, and then will balance over the pods.
  • Using a service IP, you can dynamically load balance inside a cluster using a service of type ‘ClusterIP’ or expose it to the outside world using a ‘NodePort’ or a ‘LoadBalancer’ if your cloud provider supports it.
  • Creating a service automatically creates a DNS entry, which allows for service discovery inside the cluster.

Service Spec

kind: Service
apiVersion: v1
  name: my-service
  selector: # This needs to match the labels of your deployment and pods!
    app: MyApp
  - protocol: TCP
    port: 80
    targetPort: 9376



  • An Ingress is a resource that allows you to proxy connections to backend services
  • Similar to a normal reverse proxy, but these are configured via Kubernetes annotations

Ingress Spec

apiVersion: extensions/v1beta1
kind: Ingress
  name: simple-fanout-example
  annotations: /
  - host:
      - path: /foo
          serviceName: service1
          servicePort: 4200
      - path: /bar
          serviceName: service2
          servicePort: 8080


  • A Kubernetes Secret is a base64 encoded piece of information
  • It can be mounted as a file inside of a container in the cluster
  • It can also be made available as an environment variable

Secret Spec

apiVersion: v1
kind: Secret
  name: mysecret
type: Opaque
  username: YWRtaW4=
  password: MWYyZDFlMmU2N2Rm


  • A config map is a file that is stored inside the kubernetes API.
  • You can mount a config map inside a pod to override a configuration file, and if you update the config map in the API, kubernetes will automatically push out the update file to the pods that are consuming it.
  • Config maps allow you to specify your configuration separately from your code, and with secrets, allow you to move the differences between your DTAP environments into the kubernetes API and out of your docker container

ConfigMap Spec

apiVersion: v1
data: |
    secret.code.lives=30 |
kind: ConfigMap
  name: game-config
  namespace: default


  • A daemonset is a special type of deployment that automatically creates one pod on each node in the cluster. If nodes are removed or added to the cluster, the daemonset will resize to match.
  • Daemonsets are useful for monitoring solutions that need to run on each node to gather data for example.

DaemonSet Spec

apiVersion: apps/v1
kind: DaemonSet
  name: awesome-app
    awesome: true
      name: awesome-app
        awesome: true
    spec: # Yep, Pod Spec again
      - name: awesome-app
      - name: varlog
          path: /var/log



  • StatefulSets (formerly PetSets) are a way to bring up stateful applications that require specific startup rules in order to function.
  • With a StatefulSet, the pods are automatically added to the Cluster DNS in order to find each other.
  • You can then add startup scripts to determine the order in which the pods will be started, allowing a master to start first and for slaves to join to the master.

StatefulSet Spec

apiVersion: apps/v1
kind: StatefulSet
  name: web
      app: nginx # has to match .spec.template.metadata.labels
  serviceName: "nginx"
  replicas: 3 # by default is 1
        app: nginx # has to match .spec.selector.matchLabels
      terminationGracePeriodSeconds: 10
      - name: nginx
        - containerPort: 80
          name: web



  • A namespace is a logical division of other Kubernetes resources
  • Most resources (but not all) are namespace bound
  • This allows you to have a namespace for acc and prod for example, or one for Bob and one for Omar
  • Namespace names are appended to Service names. So, to reach service 'nginx' in namespace 'bob' from namespace 'omar', you can connect to nginx.bob

Namespace Spec

apiVersion: v1
kind: Namespace
  name: default

Lab: Hands on with kubectl

Connecting to your clusters

kubectl config set-cluster workshop --insecure-skip-tls-verify=true --server https://<replace>
kubectl config set-credentials bkwi-admin --username <username> --password <password>
kubectl config set-context workshop --cluster workshop --user <username>
kubectl config use-context workshop

kubectl get node

Deploying a container on your cluster

Creating our deployment

$ kubectl run my-website --image

Using a Port-Forward to view our site

$ kubectl port-forward my-website-<randomid> 9000:80

What is it, why is it helpful and how does it work?

Now we can view it at http://localhost:9000

Patching our image

$ kubectl set image deployment my-website *
$ # Escape the asterisk if you need to
$ kubectl set image deployment my-website \*

Debugging why your container didn't work

$ kubectl get pods

$ kubectl describe pod

$ kubectl logs <name-of-the-pod>

Fixing your container

  • With the image you have from Lab 1, how can you fix this to get it working?

Fixing your deployment to use your new image

  • Using your image you built in lab01
  • Update your deployment to use the new image
  • Port forward again to view your image

Lab: Building a Wordpress site

Deploying the Kubernetes dashboard is super simple

$ kubectl apply -f

Viewing the dashboard

$ kubectl proxy

Then click here:



Getting the token to login. What is a Service Account?

Creating your namespace for your wordpress deployment

$ kubectl create namespace 101-k8s

Deploying mysql

Let's look at 03.2/mysql-deployment.yml together.


Edit the MYSQL_ROOT_PASSWORD variable to be a password of your choice


We can deploy it using:

$ kubectl create -f 03.2/mysql-deployment.yml

How do we reach our MySQL?

$ kubectl describe service wordpress-mysql

Services of type ClusterIP can be found inside the cluster by their name as a DNS record!

Deploying Wordpress

Let's look at 03.2/wordpress.yml together.


Edit the WORDPRESS_DB_PASSWORD to be what you set above.


We can deploy it using:

$ kubectl create -f 03.2/wordpress.yml

Adding a Persistent Volume

As containers are usually immutable, a persistent volume allows us to persist that data across a restart

Adding a Persistent Volume

Let's look at 03.3/persistent-volume-yml together.


We can deploy it using:

$ kubectl create -f 03.3/persistent-volume.yml

Updating our site to use our volume

Let's look at 03.3/wordpress.yml together (note, NOT 03.2!).


Edit the WORDPRESS_DB_PASSWORD to be what you set above.


We can deploy it using:

$ kubectl apply -f 03.3/wordpress.yml

$ kubectl apply -f 03.3/mysql-deployment.yml

Install your wordpress site

We can use the kubectl port-forward command we learned before to reach our websites

Lab: Bring our app to production

Adding requests and limits


(Canadian pods are very polite)

        memory: "64Mi"
        cpu: "250m"
  • Requests are the pod asking for a certain amount of RAM and CPU to be available
  • The scheduler uses this to place the pod

Pod will remain pending until space becomes available (also how the cluster autoscaler works)

Let's look at 04.2/wordpress.yml together.


Edit the MYSQL_ROOT_PASSWORD variable to be a password of your choice


We can deploy it using:

$ kubectl apply -f 04.2/wordpress.yml

Adding Requests


        memory: "128Mi"
        cpu: "500m"
  • Limits are a hard limit on the resources the pod is allowed
  • These are passed directly to the container runtime, so usually this is docker

If the pod goes over its limit it'll be restarted

Let's look at 04.3/wordpress.yml together.


Edit the MYSQL_ROOT_PASSWORD variable to be a password of your choice


We can deploy it using:

$ kubectl apply -f 04.3/wordpress.yml

Adding Limits

Adding a quota to our namespace


apiVersion: v1
kind: ResourceQuota
  name: k8s-quota
    requests.cpu: "2"
    requests.memory: 2Gi
    limits.cpu: "2"
    limits.memory: 2Gi
  • Quotas limit total resources allowed
  • Quotas are assigned to namespaces


Cluster Admin:

Let's look at 04.4/quota.yml together.


We can deploy it using:

$ kubectl create -f 04.3/quota.yml

Adding a Namespace Quota

Moving our secrets to a secret

$ kubectl -n 101-k8s create secret generic mysql-pass --from-literal=password=YOUR_PASSWORD

Creating a secret to hold our mysql password

Editing our deployment to use the new secret

Let's look at the files in 04.5 together.


We can deploy it using:

$ kubectl apply -f 04.5/

Making our app available to the internet

Exposing our wordpress to the world

$ kubectl get svc wordpress
wordpress   <none>        80/TCP    3m

Let's edit that and make it type 'LoadBalancer'


$ kubectl edit svc wordpress
service "wordpress" edited
$ kubectl get svc wordpress -o wide
NAME        CLUSTER-IP       EXTERNAL-IP                                                               PORT(S)        AGE       SELECTOR
wordpress   80:30867/TCP   4m        app=wordpress,tier=frontend

At the moment our service is type 'ClusterIP'

And now we can view our website on the web!

Questions and wrap up

Credits, License and Thanks

Thanks to:

Omar Wit, for helping to build this deck


You may reuse this, but must credit the original!

Docker & Kubernetes Workshop 101

By Andy Repton

Docker & Kubernetes Workshop 101

An introduction to containers and Kubernetes

  • 131
Loading comments...

More from Andy Repton