Introduction & Project Setup
Hey there. Today I'm going to show you how to deploy a Nest.js application using Docker and Kubernetes. So we're going to Dockerize a Nest.js application and then use it to deploy it into a Kubernetes cluster. And we're going to use Google Cloud Engine to be able to actually deploy this into a production environment. So let's go ahead and get started.
I'm going to use the Nest CLI to generate a new project and I'm going to call it nest-k8s, which stands for Kubernetes here. So let's go ahead and actually scaffold this with npm and wait for the project to initialize.
So once we've finished installing the dependencies, we can cd into our app folder and then run npm run start:dev to start the app up. And then you can go ahead and open up the app in a code editor of your choice. And so we just have a basic Nest.js server running here with our app controller, with our simple get route here. And just to make sure everything is running up okay, open up Postman and we should be able to make a request to localhost port 3000 and see the return "Hello World" from our get route.
Crafting a Multi-Stage Dockerfile
So the first step in deploying this application to a Kubernetes cluster is building it into a Docker image. So to do that, we're going to go ahead and create a Dockerfile here. And this Dockerfile is essentially a formula or a set of instructions for how to build our application in any environment.
So to get started, we're going to use the FROM keyword here to base our image off of node:alpine, which is a lightweight Linux distribution that will include Node out of the box, which is what we're going to need to run the app. And then we're going to make sure we label this stage of the build as development. This is going to be a multi-stage build, and we'll see why in a second here. So now that we are extending this node:alpine image, we're going to specify the working directory for our app. In this case, it's going to be /user/source/app. So now that we have a working directory, all subsequent commands will be relative to this directory.
So now what I want to do is I want to copy, using the COPY keyword, and I want to copy all of our package.json files. So in this case, it'll be the package.json and the package-lock.json. I want to copy that into the root directory, which is currently our working directory. And now that we've moved the package.json over, I want to run npm install to install all of our dependencies. Now, once we have all of our dependencies available, we're going to go ahead and copy the entire application, so all of our files that we currently have, we're going to move them into our working directory. And then lastly, we're going to run npm run build to actually build the Nest.js application.
So now we've actually built the Nest.js app, we're going to go ahead and declare the next stage of our build process here. So we're still going to extend the node:alpine image, but we're going to label this stage as production. And these labels can be anything you'd like; it's just to separate out the different build stages. So next, we're going to set an ARG here, and this is going to be the NODE_ENV. So this ARG is just scoped to this Dockerfile. So to actually pass it to our build process, we're going to pass the ENV keyword and then specify the NODE_ENV is equal to the ARG that we just defined, NODE_ENV, which will of course be production.
And now we're going to go ahead and actually just copy the WORKDIR command from up here. We want the same working directory for this new build stage. So every time we declare a new build stage, all the previous commands are no longer relevant, so we need to redeclare the working directory. Additionally, we're going to copy over our package.json just as we've done up here. And now we're going to run npm install again as we did before, but now we're only going to install the core dependencies in our package.json. We don't want to install any of the dev dependencies. And we used these dev dependencies to build the application in the first place, but we don't need them necessarily when we run the app. So we can specify that we only want these core dependencies that are required by the app when it runs. And the reason we do this is to keep our image as light as possible.
So after we've installed the dependencies required to run the app, we can copy over all the files as we've done before. And lastly, this most crucial stage is we are going to COPY --from=development, the first stage of our process here, we're going to copy over /user/source/app/dist. So remember up here when we ran the build command, this outputted the Nest.js application to our working directory, /user/source/app, and then /dist. So we want to copy that dist folder over to our current working directory in the production build stage. So now we have the Nest.js application built and only the dependencies required to run it, we can finally specify the command to execute and run the app, which in this case will be node and then we can specify dist/main, which is the file that is executed to start the Nest.js application. So these are all the steps required to package our Nest.js application into a Dockerfile, and now we can actually use it in a Kubernetes environment. So let's go ahead and see how we do that.
Preparing the Local Kubernetes Environment
So to set up our Kubernetes cluster, first off, make sure that you actually have a cluster available. So if you're using Docker Desktop, it actually comes with the ability to start up a Kubernetes cluster. You just need to go into your settings for Docker Desktop, Kubernetes, and make sure you've enabled Kubernetes, which will start a single-node cluster.
So once your Kubernetes server is running, we should be able to run kubectl get namespaces and see your default namespace, which means we're ready to actually work with Kubernetes. So let's go back into our code editor and I'm going to create a new folder here called k8s, which will host all of our Kubernetes related manifests.
Defining the Kubernetes Deployment Manifest
The first thing we're going to need to create is a Deployment, which describes to Kubernetes the application we want to create. Now we're just going to supply some default fields here. The apiVersion will be apps/v1, the kind will be a Deployment. So a deployment will ensure that we always have the specified number of replicas of pods running, which are essentially instances of our application.
So we're now going to specify some metadata here and just provide the name of this manifest. We'll call it nest-k8s. And now we will add the actual spec here. So we need to provide a selector for the deployment to tell it to match the pods that we're going to end up creating. So we're going to have a matchLabels here and we're going to match the label app. We're going to call this nest-k8s.
And now we can actually specify the replicas. So this is the number of instances we want of our application running. So in this case, we'll just do two. And then we provide the actual template for the pod that will be running. So now we'll provide this set of metadata on here and we'll give it a label. So now this is the label that our deployment will match. So we want to have the same label here so that our deployment is correctly matching this pod, which actually will run our Nest.js container.
So now we've specified the metadata, we'll have the spec of the actual pod and we'll specify the containers that we'll be running here. So we're only going to have one container. We can give it any name, I'll stick with our nest-k8s pattern. And now we're going to specify the name of the image that we're going to run. Now I'm going to call this mway/nest-k8s, and we're going to see how we can actually deploy this image in a second here. Let's go ahead and finish by adding a port. So we know our Nest.js application is running on port 3000, so we need to expose that port in the pod that's running. So we can specify a containerPort and give it a port of 3000.
Building and Pushing the Docker Image
So now we have the manifest to actually create our pods or instances of our application, we need a way to actually build the Docker image we specified and use it here in this manifest. So in order to push our image to a central repository, you can create a free Docker Hub account. So go hub.docker.com, go ahead and create an account, and then once you've done that, we can hit the Create Repository button. And then this is where we specify the account name. So my username was emway, and then you can give the repository a name, whatever name you'd like. I just called it nest-k8s. And then go ahead and create this repository.
So after you've gone ahead and created that repository, you want to make sure you've actually logged in on the command line. So you can use docker login and enter your credentials that you used when you created your Docker Hub account. So now that we have a Docker Hub account and a place to actually push our image up to, we need to actually go ahead and build it. So we can use docker build, give it a tag name. And this is going to be the name of the repository that we just created. So in my case, it's mway, you would replace this with your repository name, and then the name that you gave the repository, so I used nest-k8s. And then the path to where the Dockerfile lives. So in this case, it's a relative path. And then we're going to go ahead and just build this Docker image.
So you can notice here Docker is actually going through each of the steps we specified in our Dockerfile and it's building our application so that it can run it inside of a container. Now it's important to know that Docker actually is going to cache each one of these individual steps, so it'll be quicker next time we run this because it does implement caching on each one of these steps.
Now after we've finished building the Docker image, we're going to run docker push and then specify the name that we just specified in the tag. So I'm going to push this to the repo that I've created, mway/nest-k8s. And then go ahead and wait until all your layers are finished pushing to the repository.
Deploying and Verifying the Application Locally
So now that our Docker image is pushed up to the Docker Hub repository, this image here that we specified in our deployment will correctly pull from the repository we specified. By default, Kubernetes will use the Docker Hub repository, so it knows to match this with the repository that we created.
Now let's go ahead and actually deploy our application locally. We're going to cd into the k8s folder we created, and then we can use kubectl create and then specify the file, in this case deployment.yaml. So actually after we create the deployment, we can run kubectl get pods, and we can see our two replicas here are running locally. So we can use kubectl logs and then paste the name of the pod, and we can see our Nest.js application logs are outputted here.
Creating a Service for Local Access
So now we've created our pods, we need a way to communicate with them, and to do that, we're going to create a Service. And what a service does is it will give us an IP address that we can use, and then it will load balance each request to a given pod that is running the container that we want. So in the k8s folder, we'll create a service.yml file.
We're going to specify another apiVersion here, v1. I'll give it a kind of Service. We'll specify the metadata here, and as we've done before, we'll give it a name. I'll call this nest-k8s as we have been doing. And now we have the spec here. So I'm going to give this a selector. So this is going to tell the service which deployment we want to target when a request comes in. So in our case, we gave our deployment a name of app: nest-k8s. So let's go ahead and specify that selector here so that a request is routed properly.
Now that we've done this, we need to specify the ports that our service will listen on. The protocol here will be TCP, and we of course know that the port that we want to target is port 3000, which is where our Nest.js application is listening for requests. Lastly, we're going to specify the type here, and this is going to be a NodePort. A NodePort will open up this service for requests on each node in our cluster. In this case, we only have a one-node cluster, this local machine I'm running Kubernetes on, but in a real cluster, this will allow us to use any node IP address to communicate to our application.
So let's go ahead and open up the terminal again and we'll do kubectl create and then we'll pass the service.yaml. We then should be able to run kubectl get service and see our nest-k8s service here, type NodePort. So if we've set everything up correctly, we should be able to open up Postman and execute a request on localhost, and we're going to use the port specified here. This will be different for you. This port here is the node port we want to talk to. So if we execute this GET request, we can see our "Hello World" response from our Nest.js application, which is great.
Creating a Cluster on Google Kubernetes Engine (GKE)
So now that we've successfully deployed this application locally using a Kubernetes cluster, we want to actually deploy this on the cloud so we can use this API in the real world and in production. So in order to accomplish this, we're going to use Google Kubernetes Engine, which is a Kubernetes provider, a really good one, easy to use. And new customers actually get up to $300 in free credits, so you can totally try this out completely free and see how it works.
If you're not logged in, you should see a Try Free button here. Otherwise, you can go straight to your console. So after you've created or signed into a Google Cloud account, head inside the console and you can click up here to select a project or create a new one if you don't have one already. So I've created one called nest-k8s and that's the one I'll be using here. Now after that, we can search for the Kubernetes Engine project, and this is what we want here.
So if this is your first time with Kubernetes Engine, you'll just want to click the blue button that says Enable this API and wait for it to become enabled. After that, we can see this page here where we select to create a cluster. And we want to use the Autopilot cluster option, which will automatically scale and configure our cluster for us. So go ahead and click GKE Autopilot and configure that cluster. Now we can have some options where we specify the name and the region and make sure this is a public cluster. So after that, we can click create, and Google Cloud will automatically begin provisioning our cluster for us.
Connecting to Your GKE Cluster
Now, this is going to take a little bit of time for the cluster to finish becoming provisioned. And in the meantime, we want to make sure we have the Google Cloud SDK installed on our machine so that we can communicate with the cluster. So I'll include a link to these docs in the description, where it describes how to install Google Cloud SDK if you don't already have it. Essentially, we need a supported version of Python on our machine, and then there are instructions here based on your operating system. So in my case, I had the macOS 64-bit. We simply need to install the package and move it into your user's directory, your home directory, open up and unzip this file, and from there we just need to run these scripts specified. So this install script and lastly, we'll run the init script here. So after you have done that, you should be able to open up your terminal and run gcloud.
So while this is still provisioning, we should be able to still connect to the cluster. We can click on the name of the cluster here, and then we want to click on the Connect button up here and get this command for command line access so that we can use the kubectl command in our cluster. So simply, we're just going to copy this command specified and paste it into our terminal here. And this command will automatically update our kubeconfig to use this newly created cluster. But once our cluster is fully provisioned and we see the green check mark, we can go back to our command line here and run kubectl get namespaces, and we should see our newly created namespace three minutes ago. And we won't have any pods in our default namespace, which makes sense.
Deploying to Production with a LoadBalancer Service
And so now we're going to go ahead and deploy our Kubernetes manifests. However, there's one small change that we want to make to be able to deploy our service here. Now we don't want to use a NodePort, but we actually want to take advantage of Google Cloud's load balancer features. So if we specify a LoadBalancer as the type of the service here, Google Cloud will automatically provision us an external URL that we can use to make requests to our service, which is exactly what we want to communicate to our app.
So with this small change, we can now run kubectl create --file and then just provide our whole directory here. And we should see our deployment and our service get created properly. Now if we wait a little bit of time for our pods to get provisioned, we should be able to see our two Nest.js pods running properly. And if we run kubectl get service, we should see our load balancer service. Now notice we have an external IP provisioned here, and this is what we're going to use to actually make a request to our application.
So go ahead and copy this external IP address, and we can actually use the target port here to talk to this service. So if you open up Postman and enter our external IP and give it the port of 3000 and send off this request, we can see we get a "Hello World" response back from our Nest.js application, which is great to see our app has been successfully deployed in a production Kubernetes environment.
So that's all for this one. And if you have any questions or problems setting this up, please let me know in the comments. Leave a like and be sure to subscribe for the next one.