PART II – Deploying NGINX (And A Service)

In Part I we built the hardware and installed K3s on our Raspberry Pi cluster. Now we are going to deploy some pods and a service.

The first thing we are going to do is deploy 12 NGINX servers across our cluster. We do this with a manifest file that we deploy on the master node.

Manifest: Specification of a Kubernetes API object in JSON or YAML format. A manifest specifies the desired state of an object that Kubernetes will maintain when you apply the manifest. Each configuration file can contain multiple manifests.

K8s Documentation

You can snag the two yaml files that we are going to be using from my GitHub repo here.

Log into the master node and create a file named nginx.yaml file with the following contents.

apiVersion: apps/v1
kind: Deployment
  name: nginx
  namespace: default
  replicas: 12
      app: nginx
        app: nginx
      - name: nginx
        image: nginx:stable
        imagePullPolicy: Always
        - containerPort: 80
        name: web

Some of what we are doing with this manifest file is defining our App nginx, how many replicas we want, 12 as well as the port, 80 and where we want to grab our nginx image from. With this line: image: nginx:stable we are going to go out and download the stable nginx image from hub.docker.com. You can view this here: https://hub.docker.com/_/nginx

This is a super high level view of what is going on. The K8s documentation is excellent, so if you want to dive deeper, you can.

Now that we have our manifest file ready, we can deploy it!

kubectl apply -f nginx.yaml

deployment.apps/nginx created

That’s it! Now we can view the pods with the following command.

kubectl get pods -o wide
nginx-57d876fbcb-4gxkr   1/1     Running   9          16d   rpi2   <none>           <none>
nginx-57d876fbcb-zcfbw   1/1     Running   9          16d   rpi2   <none>           <none>
nginx-57d876fbcb-c8sth   1/1     Running   9          16d   rpi2   <none>           <none>
nginx-57d876fbcb-482nt   1/1     Running   9          16d   rpi4   <none>           <none>
nginx-57d876fbcb-6s2sv   1/1     Running   10         16d   rpi1   <none>           <none>
nginx-57d876fbcb-qv8bg   1/1     Running   10         16d   rpi1   <none>           <none>
nginx-57d876fbcb-lh6dn   1/1     Running   10         16d   rpi1   <none>           <none>
nginx-57d876fbcb-5tkbj   1/1     Running   9          16d   rpi4   <none>           <none>
nginx-57d876fbcb-vgcdj   1/1     Running   11         16d   rpi5   <none>           <none>
nginx-57d876fbcb-529lw   1/1     Running   11         16d   rpi5   <none>           <none>
nginx-57d876fbcb-v99r6   1/1     Running   11         16d   rpi5   <none>           <none>
nginx-57d876fbcb-v62jk   1/1     Running   9          16d   rpi4   <none>           <none>

Magic. Right? K3s is powerful and super easy to use. Trying to get K8s working on bare metal is a bear of a task, K3s definitely make it easier.

We are not done yet. Now we need to be able to access these notes. Notice above that the IP address are not on our subnet. For example These are cluster IP’s and they are only available within the cluster. So what we need to do is expose our app to the rest of the network. To do that we are going to use a service. I would stop right now and go read this. It will be worth understanding what a service is.

On the master node, create a file named nodeport.yaml with the following contents.

apiVersion: v1
kind: Service
  name: nginx-nodeport
  namespace: default
  type: NodePort
    app: nginx
    - name: web
      port: 8080
      targetPort: 80
      nodePort: 31234

Basically we are going to port forward the external port 321234 to port 80 on our web server. Now let’s deploy!

kubectl apply -f nodeport.yaml

deployment.apps/nginx-nodeport created

To see if it really worked you can run the following command.

kubectl get services
NAME             TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          
kubernetes       ClusterIP       <none>        443/TCP          
nginx-nodeport   NodePort   <none>        8080:31234/TCP   

Now for the real test. Visit a node with the 31234 port in a browser. For example: You should see the default nginx page.


When I first ran through the tutorial, so much was just not working. This was due to a few things. First, between the author publishing the tutorial video and my attempt, a major release happened. Lots changed. As well, the manifest files just were not working. Not sure why that is, but they were not. So I scrapped those and built them from scratch using the documentation.

The biggest issue was the nodeport. I could not for the life of me reach the web server on my network. If you run into this as well, hopefully the following will be helpful.

I was getting a connection refused. I have been in IT long enough to know that message means I am actively being denied. It’s not a 404 or a random config error. Something is blocking me. See below.

$ curl
curl: (7) Failed to connect to port 31234: Connection refused

So I took a peak at the firewall, and the last line explained it.

Chain KUBE-SERVICES (2 references)
target     prot opt source               destination
REJECT     tcp  --  anywhere           /* default/nginx-nodeport:web has no endpoints */ tcp dpt:http reject-with icmp-port-unreachable

What! Why! I read through the docs and the life saving articles over at stack overflow and pieced together that when K8s is misconfigured, it adds that line to the firewall. It’s protection. So it was a misconfiguration after all. Where?

Something was up with the naming of the port. Calling it web was failing. I saw this in the error log. So I swapped “web” with the actual port, 80 and that did the trick.

One of the cool commands I used to track this down was describe.

kubectl describe svc nginx-nodeport
Name:                     nginx-nodeport
Namespace:                default
Labels:                   <none>
Annotations:              <none>
Selector:                 app=nginx
Type:                     NodePort
IP Family Policy:         SingleStack
IP Families:              IPv4
Port:                     web  80/TCP
TargetPort:               web/TCP
NodePort:                 web  31234/TCP
Endpoints:                <none>
Session Affinity:         None
External Traffic Policy:  Cluster

I asked for 12 replicas, so I should see 12 endpoints. But there were none listed. Hmmmmm. Turns out, you create a nodeport with no endpoints, because you are me and you are new to K3s, that the firewall is going to be modified. After my fix, the same command shows me my 12 endpoints. Endpoints:,, + 9

kubectl describe svc nginx-nodeport
Name:                     nginx-nodeport
Namespace:                default
Labels:                   <none>
Annotations:              <none>
Selector:                 app=nginx
Type:                     NodePort
IP Family Policy:         SingleStack
IP Families:              IPv4
Port:                     web  8080/TCP
TargetPort:               80/TCP
NodePort:                 web  31234/TCP
Endpoints:      ,, + 9 more...
Session Affinity:         None
External Traffic Policy:  Cluster

And now I can reach the page from my network.

<!DOCTYPE html>
<title>Welcome to nginx!</title>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>