How to Deploy a Next.js Application with Vultr Kubernetes Engine and Vultr Load Balancer

Updated on November 21, 2023
How to Deploy a Next.js Application with Vultr Kubernetes Engine and Vultr Load Balancer header image

Introduction

Next.js is a popular React framework for developing static websites and modern web applications. Next.js application usually deploys to a serverless platform. However, there are some scenarios that you want to deploy to your controlled server and scale it in your infrastructure. This tutorial explains how to deploy a full-stack Next.js application to Vultr Kubernetes Engine. The example application is built with Next.js and uses Prisma to connect with a MySQL database.

Here are a few advantages of this approach:

  • Deploy the Next.js application as close as possible to the database.
  • Avoid the cold-starts problem in the serverless approach.
  • Predictable pricing of cloud servers and bandwidth compared to serverless platforms.

There are three separated parts in this tutorial:

  • Part 1: Build and push the Docker image to Docker Hub Image Registry
  • Part 2: Deploy and scale the Next.js application with Vultr Kubernetes Engine
  • Part 3: Expose the Next.js application with secure SSL certificates

Prerequisites

Before you begin, you should:

  • Have an external MySQL Database
  • Deploy a Vultr Kubernetes Cluster with at least 3 nodes.
  • Configure kubectl and git in your machine.
  • Install Docker on your machine
  • Register a Docker Hub account to store the Docker Image

Part 1 - Build and push the Docker image to Docker Hub Image Registry

The Next.js application used in this tutorial is a full-stack application called next-short-urls. The source code for this application can be found at this repository. Vultr has also archived the repo here.

This is a simple URL Shortener built with Next.js, TypeScript, and Prisma. Prisma is an open-source ORM that helps you build and manage the MySQL database.

  1. Clone the source code of the example application:

     $ git clone -b v1.10.0 https://github.com/quanhua92/next-short-urls
  2. Prepare a .env file environment with the below content. This .env file is used in the building stage of the Docker image to generate static pages. The final production Docker image does not contain in this file.

     DATABASE_URL="DATABASE_URL_HERE"
     SECRET_COOKIE_PASSWORD="SOME_SECRET_PASSWORD_WITH_MIN_LENGTH_32"
     AWAIT_HISTORY_UPDATE="0"

    Here is a brief explanation of the content of .env file:

    • DATABASE_URL is a database connection string started with mysql://
    • SECRET_COOKIE_PASSWORD is an application specified password with a minimum length of 32
    • AWAIT_HISTORY_UPDATE is an environment variable that the application needs.
  3. Make sure that the DockerFile file contains the below content. This is a multi-stage DockerFile with deps, builder and runner stages.

     # Install dependencies only when needed
     FROM node:16-alpine AS deps
     # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
     RUN apk add --no-cache libc6-compat
     WORKDIR /app
    
     COPY package.json yarn.lock ./
     RUN yarn install --frozen-lockfile
    
     # If using npm with a `package-lock.json` comment out above and use below instead
     # COPY package.json package-lock.json / 
     # RUN npm install
    
     # Rebuild the source code only when needed
     FROM node:16-alpine AS builder
     WORKDIR /app
     COPY --from=deps /app/node_modules ./node_modules
     COPY . .
     # generate the prisma type
     RUN npx prisma generate
    
     RUN yarn build && yarn install --production --ignore-scripts --prefer-offline
     # Production image, copy all the files and run next
     FROM node:16-alpine AS runner
     WORKDIR /app
    
     ENV NODE_ENV production
    
     RUN addgroup -g 1001 -S nodejs
     RUN adduser -S nextjs -u 1001
    
     # Prepare the cache folder for next/image
     RUN mkdir -p /app/.next/cache/images && chown nextjs:nodejs /app/.next/cache/images
     VOLUME /app/.next/cache/images
    
     # You only need to copy next.config.js if you are NOT using the default configuration
     COPY --from=builder /app/next.config.js ./
     COPY --from=builder /app/public ./public
     COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
     COPY --from=builder /app/package.json ./package.json
     COPY --from=builder /app/node_modules ./node_modules
    
     USER nextjs
    
     EXPOSE 3000
    
     ENV PORT 3000
    
     # Next.js collects completely anonymous telemetry data about general usage.
     # Learn more here: https://nextjs.org/telemetry
     # Uncomment the following line in case you want to disable telemetry.
     # ENV NEXT_TELEMETRY_DISABLED 1
     CMD ["node_modules/.bin/next", "start"]

    Here are some configurations that are specified to the next-short-urls application. In the builder stage, RUN npx prisma generate to generate the types from the Prisma schema. In the runner stage, make a folder /app/.next/cache/images and prepare the folder permission. This is used for the Automatic Image Optimization feature in Next.js.

  4. Build the Docker Image

     $ docker build . -t next-short-urls
  5. Login to Docker Hub

     $ docker login
  6. Tag the Docker Image with your Docker Hub ID and Push to Docker Hub. Replace <YOUR_DOCKER_HUB_ID> with your Docker Hub account id.

     $ docker tag next-short-urls <YOUR_DOCKER_HUB_ID>/next-short-urls:latest
     $ docker push <YOUR_DOCKER_HUB_ID>/next-short-urls:latest
  7. Run Docker local to verify the docker image. Navigate to http://localhost:3000 to access the application

     $ docker run --rm -p 3000:3000 <YOUR_DOCKER_HUB_ID>/next-short-urls
  8. (Optional) Go to your Docker Hub dashboard and make sure that your image is private.

Part 2 - Deploy and scale the Next.js application with Vultr Kubernetes Engine

In this part, you will create a secret that contains your Docker Hub credentials and create a Deployment to deploy some pods that run your Next.js application.

  1. Create Docker Hub secret named regcred. Replace parameters in the command with your Docker Hub credentials

     $ kubectl create secret docker-registry regcred \
         --docker-username=YOUR_DOCKER_HUB_USERNAME \ 
         --docker-password=YOUR_DOCKER_HUB_PASSWORD \
         --docker-email=YOUR_DOCKER_HUB_EMAIL
  2. Create a Secret manifest file secrets.yaml with the data as the previously created .env.

     apiVersion: v1
     kind: Secret
     metadata:
       name: next-short-urls-secrets
       namespace: default
     type: Opaque
     stringData:
       DATABASE_URL: "DATABASE_URL_HERE"
       SECRET_COOKIE_PASSWORD: "SOME_SECRET_PASSWORD_WITH_MIN_LENGTH_32"
       AWAIT_HISTORY_UPDATE: "0"
  3. Run the command to create the secret.

     $ kubectl apply -f secrets.yaml
  4. Create a Deployment file deployment.yaml with the following content. Replace <YOUR_DOCKER_HUB_ID> with your Docker Hub account ID.

     apiVersion: apps/v1
     kind: Deployment
     metadata:
       name: next-short-urls-deploy
     spec:
       replicas: 3
       selector:
         matchLabels:
           name: next-short-urls-app
       template:
         metadata:
           labels:
             name: next-short-urls-app
         spec:
           imagePullSecrets:
             - name: regcred
           containers:
             - name: next-short-urls
               image: <YOUR_DOCKER_HUB_ID>/next-short-urls:latest
               imagePullPolicy: Always
               ports:
                 - containerPort: 3000
               envFrom:
               - secretRef:
                   name: next-short-urls-secrets
     ---
     apiVersion: v1
     kind: Service
     metadata:
       name: next-short-urls-service
     spec:
       ports:
         - name: http
           port: 80
           protocol: TCP
           targetPort: 3000
       selector:
         name: next-short-urls-app
  5. Run the command to create the Deployment and Service.

     $ kubectl apply -f deployment.yaml
  6. Run the command kubectl get pods to see the newly created pods. The result should look similar to:

     NAME                                      READY   STATUS    RESTARTS   AGE
     next-short-urls-deploy-764d658d49-26b5w   1/1     Running   0          62s
     next-short-urls-deploy-764d658d49-hql7q   1/1     Running   0          62s
     next-short-urls-deploy-764d658d49-nv6tr   1/1     Running   0          62s
  7. Run the command kubectl get services to see the newly created service. The result should look similar to:

     NAME                      TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
     kubernetes                ClusterIP   10.96.0.1      <none>        443/TCP   94m
     next-short-urls-service   ClusterIP   10.99.247.34   <none>        80/TCP    39s
  8. Scale your application to 10 replicas using the following command.

     $ kubectl scale --replicas=10 deployment/next-short-urls-deploy
  9. Run the command kubectl get pods to see the newly created pods. The result should look similar to:

     NAME                                      READY   STATUS    RESTARTS   AGE
     next-short-urls-deploy-764d658d49-26b5w   1/1     Running   0          3m
     next-short-urls-deploy-764d658d49-5r25t   1/1     Running   0          25s
     next-short-urls-deploy-764d658d49-hql7q   1/1     Running   0          3m
     next-short-urls-deploy-764d658d49-j6qlf   1/1     Running   0          25s
     next-short-urls-deploy-764d658d49-lvrgp   1/1     Running   0          25s
     next-short-urls-deploy-764d658d49-nv6tr   1/1     Running   0          3m
     next-short-urls-deploy-764d658d49-vkl98   1/1     Running   0          25s
     next-short-urls-deploy-764d658d49-wkclv   1/1     Running   0          25s
     next-short-urls-deploy-764d658d49-xgzs6   1/1     Running   0          25s
     next-short-urls-deploy-764d658d49-zhcdj   1/1     Running   0          25s
  10. Perform port-forwarding to access your application through the service. Navigate to http://localhost:8080 to access your application.

     $ kubectl port-forward services/next-short-urls-service 8080:80

You have successfully deployed the Next.js application with MySQL Database to Vultr Kubernetes Engine with 10 replicas in your cluster.

Part 3 - Expose the Next.js application with secure SSL certificates

In this part, you will install NGINX Ingress Controller and expose your application through a domain name with secure SSL certificates.

  1. Install ingress-nginx.

     $ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.1.1/deploy/static/provider/cloud/deploy.yaml 
  2. Go to your Load Balancers dashboard at https://my.vultr.com/loadbalancers/ and get the IP Address of the newly created Load Balancer. This is the Load Balancer created for the NGINX ingress.

  3. Create an A record in your domain DNS that points to the above IP address.

  4. Install cert-manager to manage SSL certificates

     $ kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.7.0/cert-manager.yaml
  5. Create a manifest file letsencrypt.yaml to handle Let's Encrypt certificates. Replace with your actual email.

     apiVersion: cert-manager.io/v1
     kind: ClusterIssuer
     metadata:
       name: letsencrypt-staging
     spec:
       acme:
         # The ACME server URL
         server: https://acme-staging-v02.api.letsencrypt.org/directory
         preferredChain: "ISRG Root X1"
         # Email address used for ACME registration
         email: <YOUR_EMAIL>
         # Name of a secret used to store the ACME account private key
         privateKeySecretRef:
           name: letsencrypt-staging
         solvers:
           - http01:
               ingress:
                 class: nginx
     ---
     apiVersion: cert-manager.io/v1
     kind: ClusterIssuer
     metadata:
       name: letsencrypt-prod
     spec:
       acme:
         # The ACME server URL
         server: https://acme-v02.api.letsencrypt.org/directory
         # Email address used for ACME registration
         email: <YOUR_EMAIL>
         # Name of a secret used to store the ACME account private key
         privateKeySecretRef:
           name: letsencrypt-prod
         solvers:
           - http01:
               ingress:
                 class: nginx
  6. Run the command to install the above Let’s Encrypt issuers.

     $ kubectl apply -f letsencrypt.yaml
  7. Create an Ingress manifest file ingress.yaml with the following content. Replace with the domain that you have created A record in the above step.

     apiVersion: networking.k8s.io/v1
     kind: Ingress
     metadata:
       name: next-short-urls-ingress
       annotations:
         kubernetes.io/ingress.class: nginx
         cert-manager.io/cluster-issuer: letsencrypt-prod
     spec:
       tls:
         - secretName: next-short-urls-tls
           hosts:
             - <YOUR_DOMAIN>
       rules:
         - host: <YOUR_DOMAIN>
           http:
             paths:
               - path: /
                 pathType: Prefix
                 backend:
                   service:
                     name: next-short-urls-service
                     port:
                       number: 80
  8. Run the command to create the ingress.

     $ kubectl apply -f ingress.yaml
  9. Run the command kubectl get ingress to see the newly created ingress. The result should look similar to:

     NAME                      CLASS    HOSTS               ADDRESS        PORTS     AGE
     next-short-urls-ingress   <none>   <YOUR_DOMAIN>      140.82.41.69   80, 443   37s
  10. Navigate to https://<YOUR_DOMAIN to access your application.

More Information