How to Deploy a Next.js Application with Vultr Kubernetes Engine and Vultr Load Balancer
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
andgit
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.
Clone the source code of the example application:
$ git clone -b v1.10.0 https://github.com/quanhua92/next-short-urls
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 withmysql://
SECRET_COOKIE_PASSWORD
is an application specified password with a minimum length of 32AWAIT_HISTORY_UPDATE
is an environment variable that the application needs.
Make sure that the
DockerFile
file contains the below content. This is a multi-stage DockerFile withdeps
,builder
andrunner
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.Build the Docker Image
$ docker build . -t next-short-urls
Login to Docker Hub
$ docker login
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
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
(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.
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
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"
Run the command to create the secret.
$ kubectl apply -f secrets.yaml
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
Run the command to create the Deployment and Service.
$ kubectl apply -f deployment.yaml
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
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
Scale your application to 10 replicas using the following command.
$ kubectl scale --replicas=10 deployment/next-short-urls-deploy
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
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.
Install
ingress-nginx
.$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.1.1/deploy/static/provider/cloud/deploy.yaml
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.
Create an A record in your domain DNS that points to the above IP address.
Install
cert-manager
to manage SSL certificates$ kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.7.0/cert-manager.yaml
Create a manifest file
letsencrypt.yaml
to handle Let's Encrypt certificates. Replacewith 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
Run the command to install the above Let’s Encrypt issuers.
$ kubectl apply -f letsencrypt.yaml
Create an Ingress manifest file
ingress.yaml
with the following content. Replacewith 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
Run the command to create the ingress.
$ kubectl apply -f ingress.yaml
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
Navigate to
https://<YOUR_DOMAIN
to access your application.