Set up your tools
# Virtualbox (for virtualization)
brew cask install virtualbox
# HyperKit
brew install hyperkit
# Docker
brew cask install docker
# Kubernetes CLI & kubectl
brew install kubernetes-cli
# Minikube => Local Kubernetes
brew cask install minikube
# Helm => Chart management (optional)
brew install kubernetes-helm
Writing a (minimal) Node.js app
We won't dive into the details of how to write a good Node.js app in this tutorial. Our app will have a minimal server with only one route and one method, namely GET /
. Of course, we can add as many features as we want, but for the purposes of this tutorial, we'll focus more on infrastructure with Docker, Kubernetes, and Minikube. Here's what our app will look like:
const express = require('express');
// Constants
const PORT = process.env.PORT || 3000;
const HOST = '0.0.0.0'
// App
const app = express();
app.get('/', (req, res) => {
res.send('Hello world\n');
});
app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);
We only need one npm
package, which is express
. To install it, run:
npm install --save express
Dockerizing the app
We can dockerize our app by writing a Dockerfile
, which is a set of steps Docker will run to bundle it. It looks like this:
FROM node:10
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY package*.json ./
RUN npm install
# If you are building your code for production
# RUN npm ci --only=production
# Bundle app source
COPY . .
EXPOSE 80
CMD [ "node", "index.js" ]
We'll also ignore some files, like the locally installed node_modules
. To do that we create a .dockerignore
file:
node_modules
Dockerfile
.dockerignore
npm-debug.log
Now that we're set, we need to actually build our Docker image then run the container. Since I always forget the exact commands to do so, I prefer to put those in a Makefile
. Here's what it can look like:
image-name="molamk/node-app"
build:
docker build -t $(image-name) .
run:
docker run -p 3000:80 -d $(image-name)
Now we'll build the image, then run our container. It should give us "Hello World"
response with a 200
status when we call it with curl
.
# Build the image
make build
# Run the container
make run
# Call our API
curl -v localhost:3000
# HTTP/1.1 200 OK
# X-Powered-By: Express
# Content-Type: text/html; charset=utf-8
# Content-Length: 12
# ETag: W/"c-M6tWOb/Y57lesdjQuHeB1P/qTV0"
# Date: Sat, 24 Aug 2019 21:00:43 GMT
# Connection: keep-alive
# Hello world
Cool! Now that our app is dockerized, we can tag & push the image to Dockerhub. We'll add some stuff to our makefile
to do that:
tag:
docker tag molamk/node-app molamk/node-app:latest
push:
docker push molamk/node-app
Local Kubernetes with Helm & Minikube
We'll use Helm to bundle our application as a package, ready to be deployed on Kubernetes. Here's a little more info about Helm:
"Helm helps you manage Kubernetes applications — Helm Charts helps you define, install, and upgrade even the most complex Kubernetes application.
Charts are easy to create, version, share, and publish — so start using Helm and stop the copy-and-paste." - The Helm Team
First, we need to initialize Helm, here's how we do that
# Fire up Minikube
minikube start
minikube addons enable ingress
# Initialization
helm init
# Update the repositories to their latest versions
helm repo update
After that, we create what's called a Chart which will contain the manifest files for our Kubernetes deployment and service.
helm create node-app
Now let's go into the chart generated folder node-app
and edit some yaml
. We'll set the repository to be our own Docker image that we pushed previously.
# Chart.yaml
apiVersion: v1
appVersion: "1.0"
description: Running a Node.js app with Docker, Kubernetes, and Minikube
name: node-app
version: 0.1.0
# values.yaml
replicaCount: 1
image:
repository: molamk/node-app
tag: latest
pullPolicy: Always
env:
containerPort: "80"
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
service:
type: ClusterIP
port: 80
ingress:
enabled: true
annotations: {}
hosts:
- host: minikube-node-app.local
paths: ["/"]
tls: []
resources: {}
nodeSelector: {}
tolerations: []
affinity: {}
We'll also modify the deployment.yaml
file a little bit to infer our custom port.
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "node-app.fullname" . }}
labels:
{{ include "node-app.labels" . | indent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app.kubernetes.io/name: {{ include "node-app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
template:
metadata:
labels:
app.kubernetes.io/name: {{ include "node-app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: "PORT"
value: "{{ .Values.env.containerPort }}"
ports:
- name: http
containerPort: 80
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
Now let's deploy that on Kubernetes. We'll use the Helm CLI to do so, then we'll verify that everything is set up correctly.
# Deploy
helm install node-app
# Verify that it's been set-up
helm ls
# NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
# wondering-cricket 1 Sun Aug 25 18:24:51 2019 DEPLOYED node-app-0.1.0 1.0 default
Testing the whole set-up
Now let's tell our /etc/hosts
file about our custom host minikube-node-app.local
so we can call our endpoint. We'll the call it with curl
which should return a Hello world response with a 200 status code.
# Putting our custom host into the host file
echo "$(minikube ip) minikube-node-app.local" | sudo tee -a /etc/hosts
# Calling our endpoint
curl -i minikube-node-app.local
# HTTP/1.1 200 OK
# X-Powered-By: Express
# Content-Type: text/html; charset=utf-8
# Content-Length: 12
# ETag: W/"c-M6tWOb/Y57lesdjQuHeB1P/qTV0"
# Date: Sat, 24 Aug 2019 21:00:43 GMT
# Connection: keep-alive
# Hello world
Top comments (1)
Hello! Thank you for the article, but I've got:
helm install node-app
Error: render error in "node-app/templates/serviceaccount.yaml": template: node-app/templates/serviceaccount.yaml:1:14: executing "node-app/templates/serviceaccount.yaml" at <.Values.serviceAccount.create>: nil pointer evaluating interface {}.create
minikube version: v1.5.0
Helm: v2.15.1
Maybe do you know how to fix that?