Deploy Next.js on Kubernetes Step 1: Image Building & CI/CD

Using Docker, GitHub Actions, and AWS ECR

25th July 2024

6 min read

David Hazra

David Hazra is a professional software developer based in London

Docker, GitHub Actions, and AWS ECR

Caption: Docker, GitHub Actions, and AWS ECR

Introduction

Deploying Next.js on platforms other than Vercel is a viable and cost-saving alternative. To do this in a secure and convenient way it is common to containerise your application so it can be run from anywhere in a common format.

In this blog post, we'll explore how to deploy a Next.js application on Kubernetes using a CI/CD pipeline. By the end, you'll have a solid understanding of how to containerise your Next.js application, set up continuous integration with GitHub Actions, and implement continuous delivery to AWS' Elastic Container Registry (ECR).

Prerequisites

A Next.js Application: Your Next.js application should be set up and hosted in a GitHub repository. Ensure your codebase is clean and your application runs correctly.

An AWS Account and ECR Repository: An AWS account is necessary. Within your AWS account, create an ECR repository to store your Docker images. You can do this through the AWS Management Console under the ECR service.

GitHub Actions Workflow Permissions: Set up GitHub Actions in your repository. Ensure your repository has the necessary permissions to use GitHub Actions and to push Docker images to AWS ECR. Store your AWS credentials as secrets in your GitHub repository. Go to your repository settings, navigate to "Secrets and Variables" under the "Security" section, and add your AWS access key ID and secret access key.

Continuous Integration: Create a Container Image

The first step in the process is to containerise your Next.js application so that it can be served in a repeatable fashion. This is done using Docker with a Dockerfile.

Step 1: Create a Dockerfile

First, you need to create a Dockerfile in the root directory of your Next.js project. This file contains the instructions Docker will use to build your application image. Here’s an example Dockerfile for a Next.js application:

Below is an example of a very simple image build script, more advanced concepts could take advantage of using builder images, or special build arguments to reduce memory usage.

# Official Node.js image
FROM node:20-alpine

WORKDIR /app
COPY package*.json ./
RUN npm clean-install
COPY . .

RUN npm run build

CMD ["npm", "start"]

Why Use npm clean-install: The npm clean-install command installs dependencies based on the package-lock.json file, ensuring a consistent and reproducible build. This command is faster and more reliable than a standard npm install, as it skips certain steps like generating a package-lock.json file and only installs exact versions specified in it. Using npm clean-install helps avoid potential issues from cached modules or differences in dependency versions.

Why no EXPOSE: The EXPOSE instruction in a Dockerfile is not strictly necessary for Kubernetes deployments. In Kubernetes, service configurations and deployment manifests handle port mapping and expose container ports to the external world. Therefore, omitting the EXPOSE instruction in the Dockerfile is perfectly fine when the container will be managed by Kubernetes.

(Optional) Build and Push the Image Locally

To test that your docker build will work, you can do it locally

docker build -t nextjs-app:latest .

If you'd also like to push the image to ECR yourself instead of using GitHub Actions (as explained in the next section) you can do the following:

Log in to ECR using the Docker CLI

aws ecr get-login-password --region <aws-region> | docker login --username AWS --password-stdin <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com

Then tag your docker image

docker tag nextjs-app:latest <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/nextjs-app:latest

Finally, push to the ECR repo

docker push <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/nextjs-app:latest

Continuous Delivery: Automatically Push to ECR

Pushing to ECR manually is less secure and less convinient than having an automated process do the build and push cycle for you.

There are many options for automating workflows (Jenkins for example), but perhaps the simplest to get running is Github Actions.

Create a GitHub Actions workflow file in your repository. This file defines the steps to build and push the Docker image to AWS ECR. Create a directory named .github/workflows in the root of your repository and add a file named deploy.yml with the following content:

name: NextJS CI

on:
  push:
    paths:
      - "website/**"
      - ".github/**"

jobs:
  image-build-push:
    runs-on: ubuntu-latest
    
    env:
      AWS_DEFAULT_REGION: eu-west-2
      ECR_REPOSITORY: nextjs-app

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Use Node.js 20
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Install dependencies
        working-directory: website
        run: npm clean-install

      - name: Run lint
        working-directory: website
        run: npm run lint

      - name: Get short SHA
        run: |
          SHORT_SHA=$(echo $GITHUB_SHA | cut -c 1-6)
          IMAGE_TAG=${{ github.run_number }}_$SHORT_SHA
          echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV

      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_DEFAULT_REGION }}

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build docker image
        working-directory: website
        run: docker build -t ${{ steps.login-ecr.outputs.registry }}/$ECR_REPOSITORY:$IMAGE_TAG .

      - name: Push image to ECR
        if: github.ref == 'refs/heads/master'
        run: docker push ${{ steps.login-ecr.outputs.registry }}/$ECR_REPOSITORY:$IMAGE_TAG

      - name: Print ECR image URL
        if: github.ref == 'refs/heads/master'
        run: echo ${{ steps.login-ecr.outputs.registry }}/$ECR_REPOSITORY:$IMAGE_TAG

We can read the workflow as follows:

  • Begin on push when the website content has changed or the workflow has changed
  • Checkout, install, and run automated tests and linting
  • Get the git commit hash to use as a suffix for the image tag, this will help later on and is standard practice
  • Log into ECR
  • Build and push the image

What Next?

You now should have successfully pushed your Next.js image to ECR and can begin using it in your Kubernetes environment! (part 2 to come)