Article

Table of Contents
Theme:
Was this article helpful?

3  out of  3 found this helpful

Try Vultr Today with

$50 Free on Us!

Want to contribute?

You could earn up to $600 by adding new articles.

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

Author: Quan Hua

Last Updated: Fri, Feb 18, 2022
Kubernetes Load Balancer Node.js Programming

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 <YOUR_EMAIL> 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 <YOUR_DOMAIN> 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

Want to contribute?

You could earn up to $600 by adding new articles.