Skip to main content

Running Kubernetes on Ubuntu 18.04

·6 mins

Kubernetes has ostensibly become the defacto leader in the container orchestration world. If you want to run a bunch of containers across multiple nodes in a cluster, k8s is the way to go.

I really wanted to convert some of the web-based services I currently run on Docker on my home network into Kubernetes Deployments. Why? Why not?

I aimed to create a basic setup of 1 master and 2 worker nodes so I needed 3 machines. I used virtual machines to do this, but it would work just as well with any 3 machines. The main requirement is that they can all talk to one another.

This is pretty much the steps I used to get up and running, but there are probably some details or nuance missing. Leave a comment if you think I could improve the info!

From here on out, I will assume you have 3 servers running Ubuntu 18.04 with all the latest updates. My servers are called k8s-master, k8s-node1 and k8s-node2.

Network Setup #

My home network assigns IPs via DHCP in the range - which leaves 30 IPs in the range - that can be used as static IPs. You don’t have to use static IPs for any of this but it makes things way simpler.

Assign Static IPs #

The way you do this in Ubuntu 18.04 has changed from older versions. You now use netplan to generate all the configs for the actual things that do the network configuration under the hood ¯\_(ツ)_/¯.

Here’s an example of setting a static IP for my k8s-master machine. I just edited /etc/netplan/50-cloud-init.yaml that was there by default:

    version: 2
    renderer: networkd
            dhcp4: no
            addresses: []
                addresses: [,]

Then run sudo netplan apply (note that if you were connected over SSH, you might need to reconnect now, using the new IP).

Preparing the Servers #

Installing Dependencies #

All nodes will need docker-ce, kubeadm, kubelet and kubectl. You can install these by adding the package repos and then installing via apt.

# This is all easier if you just become root for a minute
sudo -i

apt update
apt install -y apt-transport-https

# Add the signing keys for kubernetes and docker-ce
curl -s | apt-key add -
curl -s | apt-key add -

# Add the repos
add-apt-repository "deb [arch=amd64] bionic stable"
add-apt-repository "deb [arch=amd64] kubernetes-xenial main"

# Install all the things
apt-get update
apt-get install -y docker-ce kubeadm kubelet kubectl

# Ctrl-D to drop root privileges!!!!

I usually like to sudo reboot after all of this. I don’t think it’s really needed, but call me superstitious.

Turning off swap #

Kubernetes refuses to start if the host node has swap enabled.

# Disable swap systemwide
sudo swapoff -a

# Edit /etc/fstab and remove any swap entries you find to make it stick
sudo nano /etc/fstab

# Verify by looking in /prod
cat /proc/swaps

Initializing the Master #

We use kubeadm to initialize a new k8s cluster. Pass the IP of your k8s-master node in as the apiserver-advertise-address and as the pod network CIDR (IMPORTANT if you’re using flannel).

# On k8s-master
sudo kubeadm --apiserver-advertise-address= \

Note: the specific Pod CIDR is required to make flannel work (see below).

Deploying a Network #

I decided to use Flannel since it is best supported by MetalLB (see below).

kubectl apply -f \

Now double check that everything is running. You should see some coredns pods with status Running if you run kubectl get pods -A.

Load Balancing with MetalLB #

Running kubernetes on bare metal is great, until you get to the Load Balancing phase. The “easiest” way to get to your services is to deploy them using the NodePort type.

Unfortunately that means that if your pod gets moved to another node, any external references to the node IP will now be wrong.

MetalLB is an open source project which makes clever use of ARP to essentially assign IPs on your network to kubernetes service endpoints. An example can be found towards the end of this post.

Deploy the metallb system using:

kubectl apply -f \

Then you can use kubectl get pods -n metallb-system to make sure those pods are up and running correctly:

NAME                         READY   STATUS    RESTARTS   AGE
controller-cd8657667-s9s97   1/1     Running   0          3d11h
speaker-jw8qr                1/1     Running   1          3d12h
speaker-lsln7                1/1     Running   1          3d12h

Then it’s time to tell metallb what it should do. We want it to assign IPs in the range and so:

# metallb-config.yaml
apiVersion: v1
kind: ConfigMap
  namespace: metallb-system
  name: config
  config: |
    - name: my-ip-space
      protocol: layer2

Now apply this to your cluster using kubectl apply -f metallb-config.yaml.

Joining Nodes to the Cluster #

So now you have a k8s-master server running, with a network overlay, and a provider for LoadBalancer service endpoint types.

This command can be found in the output of kubeadm init from your master:

# run this from k8s-node1 and k8s-node2
kubeadm join --token <your token> \
    --discovery-token-ca-cert-hash sha256:<some hash>

You can check out the Node status by using kubectl get nodes.

Deploying an Example App #

Now we’re at the point where we can start deploying stuff into our new cluster!

Below is the yaml config 4 replicas of an image called nginxdemos/hello:plain-text. This outputs some info about the server the nginx process is running on (in this case, the name of the pod!).

apiVersion: apps/v1
kind: Deployment
  name: nginx-deployment
    app: nginx
  replicas: 4
      app: nginx
        app: nginx
      - name: nginx
        image: nginxdemos/hello:plain-text
        - containerPort: 80

apiVersion: v1
kind: Service
  name: nginx
  - name: http
    port: 80
    protocol: TCP
    targetPort: 80
    app: nginx
  type: LoadBalancer

Note the use of the type: LoadBalancer in the spec section of the app config.

Now let’s make sure the pods are up and running:

$ kubectl get svc,pods

NAME                 TYPE           CLUSTER-IP      EXTERNAL-IP      PORT(S)        AGE
service/kubernetes   ClusterIP       <none>           443/TCP        3d12h
service/nginx        LoadBalancer    80:32178/TCP   3d11h

NAME                                   READY   STATUS    RESTARTS   AGE
pod/nginx-deployment-c9fdccf48-hp2nr   1/1     Running   0          3d10h
pod/nginx-deployment-c9fdccf48-rbnlp   1/1     Running   0          3d10h
pod/nginx-deployment-c9fdccf48-rwmjw   1/1     Running   0          3d10h
pod/nginx-deployment-c9fdccf48-srl8f   1/1     Running   0          3d10h

Now confirm that your service is getting load balanced properly!

$ curl

Server address:
Server name: nginx-deployment-c9fdccf48-hp2nr
Date: 11/May/2019:14:34:14 +0000
URI: /
Request ID: 370c7a421e3de42122400d11f43155de

It gave us Server name: nginx-deployment-c9fdccf48-hp2nr. Now hit it again:

$ curl

Server address:
Server name: nginx-deployment-c9fdccf48-rbnlp
Date: 11/May/2019:14:34:15 +0000
URI: /
Request ID: 10ef77ca7166af171ad1f6979e594dc0

BOOM: Server name: nginx-deployment-c9fdccf48-rbnlp.

Conclusion #

Now we can deploy anything into our cluster, and it will get its own IP on your network!

As an exercise for the reader, try forcefully powering down one of your nodes.

Did any of your pod replicas automatically migrate to the other worker node? If they didn’t, try figure out why not and then try make it so that happens!

c o n t a i n e r s