Building a web server: Containers
Tags: docker, kubernetes, helm, learning
Published: 2024-06-07 19:47:30

Welcome back to This Old Box! A series that covers the journey of building and running a web server out of an old 2014 Mac mini. 

In this post we'll walk through how we containerized our first web application, configured our Kubernetes cluster with our new container, and exposed that cluster to the world.

The project we deployed is the most simple from an infrastructure standpoint. It's a static site that is mainly html, css, and a wee bit of client-side JavaScript. Logistically, the whole site fit into a single container. 

## Docker
There are several container runtimes in the industry today. We chose   to use [Docker](https://www.docker.com/) as it seems to be the industry standard, especially on a Mac. Docker is also well documented, and thus should be the easiest to learn. Although, I am concerned because I've heard the runtime is a bit greedy with CPU and memory resources. I hope this Old Box can handle it.

We installed [Docker Desktop](https://www.docker.com/products/docker-desktop/) in [our last post](https://dev.to/stmcallister/building-a-web-server-installing-the-software-gjb). With that application running we built a docker image by writing the instructions in a `Dockerfile`. 

This first website we containerized is a collection of static sites that my son created called [AjMcWeb](https://www.ajmcweb.com/). The instructions for building the container for this site were relatively simple. All we really needed to do was copy all the html, javascript, css, and image files for the site into the container, run them on `nginx`, and expose port 3001 on the container. The `Dockerfile` to accomplish these steps looked like this.

```docker
FROM nginx
COPY . /usr/share/nginx/html
EXPOSE 3001
```

Using that `Dockerfile`, we ran the following `docker build` command inside the same folder to build a running image of that container.

```bash
docker build -t ajmcweb:0.1 .
```

This command built the Docker image. The `-t` argument told Docker to tag the image with the the name `ajmcweb` and gave it a version of `0.1`. The trailing `.` told Docker to use the Dockerfile in the current directory.

Now that we had our site encapsulated inside a Docker container we needed to deploy it to the world. That's where Kubernetes came in.

## Kubernetes
Docker Desktop includes a standalone Kubernetes server and client that we enabled [in our last post](https://dev.to/stmcallister/building-a-web-server-installing-the-software-gjb). This enablement instantiated the images required to run the Kubernetes server as containers, and installed the `kubectl` utility--used to manage our Kubernetes cluster--on our machine.    

## ngrok for ingress
Before configuring our Kubernetes cluster, we needed to install the [ngrok Ingress Controller](https://github.com/ngrok/kubernetes-ingress-controller?tab=readme-ov-file#ngrok-kubernetes-ingress-controller) so we could use [ngrok](https://ngrok.com/) to provide ingress to our application. [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) is "an API object that manages external access to the services in a cluster." 

ngrok is an ingress platform that helps provide access to a variety of things including Kubernetes clusters as well as devices and other networks. The reason why we chose ngrok for this particular project is because [ngrok works behind a NAT](https://ngrok.com/blog-post/ngrok-ingress-controller-differentiators#the-ngrok-kubernetes-ingress-controller-works-behind-nat).

Here's how we set up ngrok and its ingress controller. 

## Setting up ngrok
First, we signed up for a [free ngrok](https://ngrok.com/signup) and then upgraded to a [paid plan](https://ngrok.com/pricing) so we could use our [own domains](https://ngrok.com/docs/guides/how-to-set-up-a-custom-domain/) for our sites--like [www.ajmcweb.com](https://www.ajmcweb.com/) for example.

In order to use ngrok we first needed to get our [ngrok authtoken](https://ngrok.com/docs/agent/#authtokens) in our ngrok dashboard. Then, we used that to set an `NGROK_AUTHTOKEN` environment variable on our system.

Next we [created an API Key](https://ngrok.com/docs/agent/#api-keys) and set that to an `NGROK_API_KEY` environment variable.

The last environment variable we set was `NAMESPACE`. This is going to be the Kubernetes namespace we'll use for our clusters on this box. In our case, we used `macbox`.

Both of these environment variables will be used when we install the ngrok ingress controller. But, before we leave ngrok, I'll mention how we set up our own domains inside of our ngrok account. 

### Setting up custom domains in ngrok
We followed the [ngrok guide for setting up Custom Domains](https://ngrok.com/docs/guides/how-to-set-up-a-custom-domain/) which can be boiled down to two steps. First, we went to the Domains section of the [ngrok dashboard](https://dashboard.ngrok.com) and clicked the New Domain button. 

We typed in `www.ajmcweb.com` and clicked Continue. This gave us a Domain Record value that we copied and pasted into a CNAME record on our domain registrar.

## Setting up ngrok ingress controller
To establish ingress, or provide user access, we wanted to incorporate the ngrok account that we configured previously. ngrok has an [ingress controller for Kubernetes]() which we installed as a helm chart with the following steps:

We added the ngrok repo to our helm settings.

```bash
helm repo add ngrok https://ngrok.github.io/kubernetes-ingress-controller
``` 

Then, we installed the helm chart with the following command, referencing the environment variables we set earlier.

```bash
helm install ngrok-ingress-controller ngrok/kubernetes-ingress-controller \
  --namespace $NAMESPACE \
  --create-namespace \
  --set credentials.apiKey=$NGROK_API_KEY \
  --set credentials.authtoken=$NGROK_AUTHTOKEN
```

### Configuring Kubernetese
Going back to our code we captured in a Docker container we were ready to define our application deployment in Kubernetes. We began with configuring a `Service` that is a pod running on `http` and port 80. 

```yaml
apiVersion: v1
kind: Service
metadata:
  name: ajmcweb
  namespace: macbox
spec:
  ports:
      - name: http
        port: 80
        targetPort: 80
  selector:
      app: ajmcweb
```

Then we defined a `Deployment` object to run our `ajmcweb` container we built earlier. In the code below you'll see that we're running version `0.3` of that container, which is denoted as `ajmcweb:0.3`.

```yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ajmcweb
  namespace: macbox
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ajmcweb
  template:
    metadata:
      labels:
        app: ajmcweb
        version: v0.1
    spec:
      containers:
        - name: ajmcweb
          image: ajmcweb:0.3
          ports:
          - name: http
            containerPort: 80
```
And, finally, we configured our ingress object using the [ngrok Kubernetes Operator](https://ngrok.com/docs/k8s/) that we installed earlier.

```yaml
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ajmcweb
  namespace: macbox
spec:
  ingressClassName: ngrok
  rules:
  - host: www.ajmcweb.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: ajmcweb
            port:
              number: 80
```

Our ingress object had one rule which defined the host as `www.ajmcweb.com` and the backend would run the `ajmcweb` service on port 80.

We applied those Kubernetes configurations with a `kubectl apply -f <filename>.yaml` and [www.ajmcweb.com](https://www.ajmcweb.com) came right up in the browser!

Success!

## Conclusion
This felt great to be able to get our simple static website deployed on our server and have ngrok handle the network settings to make the site reachable. 

Our next task will be to get a more complex application online. My son has an application he built to help track the time he spends practicing his saxophone. The app uses [Node.js](https://nodejs.org/en) and [MySQL](https://www.mysql.com/) for the backend and has a much more rich frontend. It will require some more effort both from a deployment standpoint and from a processing perspective. Stay tuned as we figure out how to get this application online and keep it running!