Related
Comparison of Video Call Services for Cross-Platform React Web/Native Apps
A comprehensive comparison of leading video call service providers focusing on cross-platform compatibility for React-based web and native...
Popular topics
06 min reading in—DevOpsWeb Development
Simple steps to build, push, and deploy a Next.js app on Kubernetes
This guide illustrates the steps required to build, push, and deploy a Next.js app on Kubernetes using docker files for defining development and production Next.js images. These docker compose files allow for the building and pushing of images, with a Next.js config able to export static files. A simple helm chart will create deployment, service, and ingress for exposing our app.
Template repo containing all the required code and configs can be found on my github.
This tutorial can also be adapted to work with any managed cluster as I've been using similar setups to quickly deploy Next.js apps for a while.
For your cluster you'll need:
cert-manger
cluster issuer with name letsencrypt-prod
setup.nginx-ingress
installed.helm
installed.Further, a private container registry is required.
If you don't have a Kubernetes cluster ready follow My Blog Post on Microk8s Private Cluster Setup.
Begin by creating a simple Next.js app:
npx create-next-app@latest
What is your project named? frontend
Would you like to use TypeScript? Yes
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes
Would you like to use `src/` directory? No
Would you like to use App Router? (recommended) Yes
Would you like to customize the default import alias (@/*)? No
We named the app ./frontend
so the respective folder is created.
Two docker files can be created:
./frontend/Dockerfile
: A development docker file that mounts frontend ./frontend
into the container for hot-reload../frontend/pro.dockerfile
: A production docker file that statically builds the entire application and Next.js in a container.The development docker file is straightforward:
FROM node:16-alpine WORKDIR /frontend COPY ./package.json . RUN apk add curl RUN npm i --save-dev ENTRYPOINT ["npm", "run", "dev"]
Here, we execute npm i
and then npm run dev
. When mounted to the host, the ./node_modules
directory will also be present on the host.
The production docker file, which is based on the Next.js example docker file, can be found here.
FROM node:16-alpine AS deps RUN apk add --no-cache libc6-compat curl WORKDIR /app COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ RUN \ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ elif [ -f package-lock.json ]; then npm i; \ elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \ else echo "Lockfile not found." && exit 1; \ fi FROM node:16-alpine AS builder WORKDIR /app COPY . . RUN apk add --no-cache curl RUN npm i RUN npm install --unsafe-perm -g sharp ENV NEXT_TELEMETRY_DISABLED 1 RUN yarn build FROM node:16-alpine AS runner WORKDIR /app ENV NODE_ENV production ENV NEXT_TELEMETRY_DISABLED 1 RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs COPY /app/styles ./styles COPY /app/public ./public COPY /app/.next/standalone ./ COPY /app/.next/static ./.next/static USER nextjs EXPOSE 3000 ENV PORT 3000 CMD ["node", "server.js"]
Creating two compose files can pave the way for convenience:
docker-compose.yaml
: A development compose file.docker-compose.pro.yaml
: A production/ Local Infrastructure compose file.Development is now always as straightforward as executing docker-compose up
.
To effectively manage the deployments and services we require, we prefer to create a simple Helm chart that can easily be adapted, installed, and updated. Our Helm chart will need the following components:
microk8s helm create ./helm
cd ./helm/template && rm -rf *
Everything will be deployed to a rootNamespace
.
We have reset the template and created a simple setup for now ./helm/template/frontend.yaml
.
{{- if .Values.registryAuth.use }} kind: Secret type: kubernetes.io/dockerconfigjson apiVersion: v1 metadata: name: dockerconfigjson-github-com namespace: {{ .Values.rootNamespace }} stringData: .dockerconfigjson: > {{ ( dict "auths" ( dict {{ .Values.registryAuth.registry }} ( dict "auth" .Values.registryAuth.token ) ) ) | toJson }} {{- end }}
We've created a simple flag .Values.registryAuth.use
to check if registry authentication should be used (as we wouldn't need it for local microk8s deployment).
We use .Values.registryAuth.use
to define the registry that should be used.
Now we define a simple deployment for our nextjs backend:
apiVersion: apps/v1 kind: Deployment metadata: name: frontend-container namespace: {{ .Values.rootNamespace }} spec: replicas: 1 selector: matchLabels: app: frontend-container template: metadata: labels: app: frontend-container spec: containers: - name: frontend-container image: {{ .Values.frontend.imageURL }} ports: - containerPort: 8000 envFrom: - secretRef: name: frontend-secrets {{- if .Values.registryAuth.use }} imagePullSecrets: - name: dockerconfigjson-github-com {{- end }}
Note that we inject the imageURL
and the registry pull secret if required. We go on to expose the port 3000
to the cluster through a simple service.
apiVersion: v1 kind: Service metadata: name: frontend-service namespace: {{ .Values.rootNamespace }} labels: app: frontend-container spec: type: ClusterIP ports: - port: 3000 targetPort: 3000 selector: app: frontend-container
It's also advisable to create a secret that injects some general environment variables listed in values.yaml:frontend.env.*
apiVersion: v1 kind: Secret metadata: name: frontend-secrets namespace: {{ .Values.rootNamespace }} type: Opaque data: {{- range $key, $value := .Values.frontend.env }} {{ $key }}: {{ $value | b64enc }} {{- end }}
Finally, we set up an ingress:
{{- if .Values.ingress.use }} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: frontend-ingress namespace: {{ .Values.rootNamespace }} annotations: nginx.ingress.kubernetes.io/proxy-connect-timeout: '3600' nginx.ingress.kubernetes.io/proxy-read-timeout: '3600' nginx.ingress.kubernetes.io/proxy-send-timeout: '3600' nginx.ingress.kubernetes.io/server-snippets: | location /ws/ { proxy_http_version 1.1; proxy_redirect off; proxy_buffering off; } {{- if .Values.ingress.certManager }} cert-manager.io/cluster-issuer: letsencrypt-prod {{- end }} kubernetes.io/ingress.class: public spec: {{- if .Values.ingress.certManager }} tls: - hosts: - {{ .Values.ingress.host }} secretName: frontend-ingress-tls {{- end }} rules: - host: {{ .Values.ingress.host }} http: paths: - path: / pathType: Prefix backend: service: name: frontend-service port: number: 3000 {{- end }}
We've introduced some flags to set up tls
if required and some annotations to facilitate websocket forwards.
Values.yaml
Finally, to tie this all together, we create some default values.yaml
rootNamespace: "default" registryAuth: registry: "localhost:32000" use: false token: "" ingress: use: true certManager: false host: "localhost" frontend: imageURL: "localhost:32000/frontend:registry" env: HOSTNAME: "localhost"
This can be used to control all the configuration options we just created.
It's always good to ensure that we can also deploy the entire infrastructure locally. For this, we can use a simple local microk8s
installation.
microk8s enable ingress dns registry
docker-compose -f docker-compose.pro.yaml build
docker-compose -f docker-compose.pro.yaml push
Since we set up the template .env
earlier, we're already configured to push and pull the image from localhost:32000/frontend:registry
.
So now, all we need to do is install our newly created chart:
microk8s helm install next-js-frontend ./helm/ --set rootNamespace="nextjsfront"
Since we also set up $ROOT_URL="http://localhost"
, the ingress will be configured to route to http://localhost
. Now let's check the browser:
For the capability to deploy to a private cluster, you'll need a package registry available.
If you quickly want a cheap registry, you can check out my blog post on deploying Gitea in a private k8s cluster.
Firstly, we need to modify the environment to push to our private registry with a modified tag.
rm .env echo "FRONTEND_IMAGE=\"<your-frontend-image-url>\"" >> .env echo "ROOT_URL=\"<your-host-url>\"" >> .env
An example of an image URL is my-gitea.example.com/gitea-private-user/package:latest
.
A ROOT_URL
could be something like nextjs.example.com
.
Note that you can restore the old default .env
at any time using git checkout .env
.
We also need to encode access credentials to our registry so we can inject them into our secret.
echo "<your-registry-user>:<your-registry-password>" | base64
docker-compose -f docker-compose.pro.yaml build docker-compose -f docker-compose.pro.yaml push KUBECONFIG="./kubeconfig.yaml" microk8s helm install next-js-frontend ./helm/ \ --set rootNamespace="nextjsfront" \ --set ingress.use=true \ --set frontend.imageURL="<your-frontend-image-url>" \ --set certManager.use=true \ --set registryAuth.use=true \ --set registryAuth.registry="<your-registry-host>" \ --set registryAuth.token="<base64-encoded-token>"
It works perfectly! In fact, this blog was deployed using this exact technique.
Now, we have a simple process to deploy the repository using GitHub Actions. These actions can also run on your own runners using Gitea Actions.
We create one simple workflow .github/workflow/deploy.yaml
name: Deploy NextJs App on: workflow_dispatch: permissions: contents: read pages: write id-token: write jobs: deploy: runs-on: ubuntu-latest name: Deploy NextJS App steps: - uses: actions/checkout@master with: token: ${{ secrets.BOT_PAT }} submodules: recursive ref: ${{ github.event.pull_request.head.sha }} - name: Setup Kubeconfig id: setup_kubeconfig run: | touch .env; rm .env echo "${{ secrets.KUBE_CONFIG_BASE64 }}" | base64 -d > kubeconfig.yaml - name: Setup Environment id: setup_env run: | touch .env; rm .env; touch .env RANDOM_STRING="$( openssl rand -hex 3 )" FRONTEND_IMAGE="${{ secrets.CONTAINER_REGISTRY }}/${{ secrets.GITTEA_USER }}/nextjs-app:build-$RANDOM_STRING" echo "FRONTEND_IMAGE=\"$FRONTEND_IMAGE\"" >> .env echo "frontend_image=$FRONTEND_IMAGE" >> $GITHUB_OUTPUT - name: Build And Push Image run: | echo ${{ secrets.GITTEA_PASSWORD }} | docker login ${{ secrets.CONTAINER_REGISTRY }} -u ${{ secrets.GITTEA_USER }} --password-stdin DOCKER_BUILDKIT=1 docker-compose -f docker-compose.pro.yaml build DOCKER_BUILDKIT=1 docker-compose -f docker-compose.pro.yaml push - uses: azure/setup-helm@v3 with: version: 'latest' token: ${{ secrets.BOT_PAT }} id: install-helm - uses: azure/setup-kubectl@v3 with: version: 'latest' id: install-kubectl - name: Setup Cluster Connection id: cluster_connection run: | echo "Test" REGISTRY_AUTH=$(echo -n "${{ secrets.GITTEA_USER }}:${{ secrets.GITTEA_PASSWORD }}" | base64) KUBECONFIG=./kubeconfig.yaml kubectl create namespace nextjsnamespace || true cat << EOF | while read eval_command; do yq -i eval "$eval_command" ./helm/values.yaml; done .rootNamespace = "nextjsnamespace" .ingress.host = "${{ secrets.BOT_PAT }}" .frontend.imageURL = "${{ steps.setup_env.outputs.frontend_image }}" .ingress.certManager = true .registryAuth.token = "$REGISTRY_AUTH" .registryAuth.use = true EOF KUBECONFIG=./kubeconfig.yaml helm upgrade next-js-app ./helm/ --set rootNamespace="nextjsnamespace" --install
Using the following secrets configured in your GitHub account:
BOT_PAT
: GitHub bot access tokenGITTEA_USER
: Private registry userGITTEA_PASSWORD
: Private registry password or auth tokenCONTAINER_REGISTRY
: Container registry host addressKUBE_CONFIG_BASE64
: Base64 encoded kubeconfig to connect to the k8s clusteractions/checkout@master
..env
with a random image tagazure/setup-helm@v3
, azure/setup-kubectl@v3
values.yaml
with our new configuration and update the Helm installationOpportunities from here are limitless; you can easily have any pull to the main deploy a feature environment or connect a backend to your Next.js app.
Related
A comprehensive comparison of leading video call service providers focusing on cross-platform compatibility for React-based web and native...
Related
Set up Slack notifications for deployments using GitHub Actions and the Python slack_sdk package, keeping your team updated automatically.
Related
Self Hosted Error Tracking and Reporting using Sentry Kubernetes and Helm