DEV Community

Cover image for Build NextJS Application Using GitHub Workflow and Docker
Yash Thakkar
Yash Thakkar

Posted on • Updated on

Build NextJS Application Using GitHub Workflow and Docker

NextJS is a JavaScript framework created by vercel. It lets you build serverless API, server-side rendering and static web applications using React. Vercel provides the out of box CI/CD integration with GitHub, GitLab, and BitHub. But sometimes, we want to host our NextJS application on other platforms than vercel, like AWS, GCP, DigitalOcean or Azure. In this blog, we will see how we can build our NextJS application using GitHub Workflow and Docker.

Setup NextJS Application

NextJS recommends using create-next-app, which sets up everything automatically for you. To create a project, run:

npx create-next-app
# or
yarn create next-app
Enter fullscreen mode Exit fullscreen mode

After the installation is completed, follow the instructions to start the development server. Try editing pages/index.js and see the result on your browser.

For more information on how to use create-next-app, you can review the documentation

Setup Dockerfile

We will package our NextJS application in Docker image. The reason for using Docker is we won't need to install any additional packages like nodejs, pm2 etc when we want to run our NextJS server. Docker will bundle everything up and give us the image that we can run anywhere. Below is the sample Dockerfile for our NextJS application.

FROM node:lts-alpine

ENV NODE_ENV production
ENV NPM_CONFIG_LOGLEVEL warn

RUN mkdir /home/node/app/ && chown -R node:node /home/node/app

WORKDIR /home/node/app

COPY package.json package.json
COPY package-lock.json package-lock.json

USER node

RUN npm install --production

COPY --chown=node:node .next .next
COPY --chown=node:node public public

EXPOSE 3000

CMD npm start
Enter fullscreen mode Exit fullscreen mode

Now, let's see what is happening in above Dockerfile step-by-step.

  • We are using node:lts-alpine as the base image.
  • Setting environment variable as production.
  • Setting up an app folder with node user as owner.
  • Copying package.json and package-lock.json into the image.
  • Running npm install production to install only production dependencies.
  • Copying .next and public folder into the container. This is a very interesting step. Why are we copying the folders and not building the the application using next build command? We will discuss this in detail below.
  • Exposing port 3000, so that our application can be accessible out of the container.
  • Finally, running npm start command to start our NextJS application server.

We can see, we are not making any changes in the Dockerfile. It's easy to understand and straightforward. The interesting part is we are copying .next and public folder into the container, instead of building inside the container.

Here is the detailed explanation:

  • In NextJS application, we may need to use NEXT_PUBLIC environment variables. NEXT_PUBLIC variables are required for the build time process. (eg. firebase web client)
  • If we use a firebase web client, then we need to provide a few required variables like firebase api_key, app_id, auth_domain.
  • We write these variables in .env or .env.local file when developing our application locally. But we DO NOT, SHOULD NOT and MUST NOT push this file on VCS systems like git.
  • So when we build our application locally, it will use these variables from the .env and process gets completed without any error. But when we build our application in Docker using RUN next build command, our build command will fail because we are not providing these variables in the docker image.
  • If we want to build our NextJS application inside docker build process, we need to use --build-args in docker build command to pass the build-time variables. There are 2 ways to do this.
    1. We use ci secret variables and pass them into the docker build command
    2. We create a .env file, encode tis using base64, pass it as ci secret variable, decode it using base64 inside docker file and then build the docker image.
  • This will become very difficult to pass and maintain if our public variables list grows in the future.
  • So to not complicate the build process, we will build our application outside the docker image using ci job and then copy the .next, public folders into a docker image.
  • To pass environment variables in ci, there are 2 ways.
    1. Pass the environment variables as secrets
    2. Pass the base64 encoded of .env file, decode it inside ci process, write the file at the root of our project folder, same as local development and build our application.

GtiHub Workflow

A workflow is a configurable automated process made up of one or more jobs. We will configure the workflow with YAML file. You can read more here.

As discussed above, we will use GitHub workflow jobs to build our NextJS application. Below is the workflow file, we will be using the same. Save this file at PROJECT_ROOT_FOLDER/.github/workflows/main.yml, so that GitHub can read the yaml file and setup actions accordingly.

Note: To be able to see the actions in the UI, you need to have the same file available in master or main branch.

Workflow file:

name: Build & Publish

on:
  push:
    branches:
      - "**"             # all branches
      - "!dependabot/**"      # exclude dependbot branches
  workflow_dispatch:      # Manually run the workflow

jobs:
  next-build:
    if: ${{ github.event_name == 'workflow_dispatch' }}       # Run only if triggered manually
    runs-on: ubuntu-latest
    container: node:lts          # Use node LTS container version, same as Dockerfile base image
    steps:
      - name: Checkout
        uses: actions/checkout@v2       # Checkout the code
      - run: npm ci            #install dependencies
      - run: npm run build
        env:
          NEXT_PUBLIC_FIREBASE_API_KEY: ${{secrets.NEXT_PUBLIC_FIREBASE_API_KEY}}
          NEXT_PUBLIC_FIREBASE_APP_ID: ${{secrets.NEXT_PUBLIC_FIREBASE_APP_ID}}
          NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN}}
          NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID}}
          NEXT_PUBLIC_SENTRY_DSN: ${{secrets.NEXT_PUBLIC_SENTRY_DSN}}
      - name: Upload Next build          # Upload the artifact
        uses: actions/upload-artifact@v2
        with:
          name: build
          path: |
            .next
            public
          retention-days: 7         # artifact retention duration, can be upto 30 days
  docker-push:
    needs: next-build        # Job depends on next-build(above) job
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Download next build       # Download the above uploaded artifact
        uses: actions/download-artifact@v2
        with:
          name: build
      - name: Login to GitHub Container Registry
        uses: docker/login-action@v1
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.CR_PAT }}
      - name: Build and Push Docker Images
        run: |
          export CURRENT_BRANCH=${GITHUB_REF#refs/heads/}
          export TAG=$([[ $CURRENT_BRANCH == "main" ]] && echo "latest" || echo $CURRENT_BRANCH)
          export GITHUB_REF_IMAGE=ghcr.io/$GITHUB_REPOSITORY:$GITHUB_SHA
          export GITHUB_BRANCH_IMAGE=ghcr.io/$GITHUB_REPOSITORY:$TAG
          docker build -t $GCR_IMAGE -t $GITHUB_REF_IMAGE -t $GITHUB_BRANCH_IMAGE .
          echo "Pushing Image to GitHub Container Registry"
          docker push $GITHUB_REF_IMAGE
          docker push $GITHUB_BRANCH_IMAGE
Enter fullscreen mode Exit fullscreen mode

Now, let's discuss what is happening in yaml file.

  • We need to pass the condition on which event we want to trigger our workflow. In our case, we want it on the push event. It can multiple as well like [push, pull_request]. You can read more here.
  • We can define the branches, we want this workflow to run to watch. ! means want to exclude these branches.
  • workflow_dispatch to manually run the build process. If we don't write this, our workflow will run every-time we push to any branch of our repository. You can read more here.
  • We have divided our build process into 2 jobs.
    1. next-build:
      • In this job, we are using node:lts as the base image, this has to be the same as Dockerfile base image
      • We are keeping this job manual, as we don't want this job to run everytime we push the code. So we add if: ${{ github.event_name == 'workflow_dispatch' }} condition in step.
      • In env section, we are exporting environment variables from secrets. So we need to add these variables in GitHub project secrets. Read more here on how to do it.
      • In next step, action will checkout the code, run npm ci to install dependencies and npm run build to build the NextJS application using exported environment variables.
      • Finally, after a successful build, CI job will use actions/upload-artifact@v2 action to upload our build folder as an artifact on GitHub with 7 days of retention time, so that ci job can download the same folder in docker-build job and use it to build the image. In the build folder, we are including .next and public folder. .next folder is generated by the build process and we use public folder for assets like, svgs, images etc. So we want to keep that folder as well.
      • You can see the folder in action detail as below.
    2. docker-push: To build our docker image
      • This job depends on needs:next-build, which means we will only see this job after a successful next-build job. If we don't write this, then our both the job will parallel and this job will fail because it won't be able to download build artifact. next build will upload the artifact then only, ci job and we will be able to access it. So we need to write this, it will create a sequential job, instead of parallel.
      • CI job will checkout the code, download the build artifact folder using actions/download-artifact@v2 and extract it as well.
      • We want to keep our docker image to be hosted on GitHub packages, for that, we will use docker/login-action@v1 action to login into the GitHub Container Registry server using username and password. We need to pass CR_PAT as well in repository secrets same as NEXT_PUBLIC vars. We can add here other registries as well like GCR, AWS ECR etc.
      • Next, ci job will get the CURRENT_BRANCH and tag our docker build accordingly. Here, we are creating 2 tags, one is with branch name like dev, qa, uat, main and another is with commit SHA.
      • after that, the job will start building our docker images and push it to GitHub packages after a successful build. Here, we can push it to other registries as well like GCR, AWS ECR etc.
      • finally, this job will exit and our workflow will be successfully passed.

To run the job, we have to navigate to repo actions and you will see the workflow with Build & Push on the left sidebar. Click on that link and you will be able to see the screen as below.

GitHub Workflow Build

With this, We will be able to build and package our NextJS application. You will see the action screen below the screenshot.

GitHub Workflow Build Complete

Thank you for reading. Have a great day!

Help Links:

Top comments (7)

Collapse
 
behnamio profile image
behnam-io • Edited

Hi Yash, thanks for sharing this post. I tried to follow your steps, but getting error on "push" step in github workflow, any idea?

invalid argument "-t" for "-t, --tag" flag: invalid reference format
Enter fullscreen mode Exit fullscreen mode
Collapse
 
thakkaryash94 profile image
Yash Thakkar

Hey,
Probably because docker build -t is not tagging the image properly. Try to run docker images after docker build command. It will print all the docker images we build with name.

Collapse
 
behnamio profile image
behnam-io

I resolved it, was hillarious but docker wasn't recognizing "-" and caps in my repository name, changing repo name to lowercase and replacing "-" with "_" solved the problem; But I'm now facing another issue, I'm only able to pass "NEXT_PUBLIC" env variables, but some libs like "next-auth" only accept some hardcoded variables like "NEXTAUTH_URL", is there anyway to solve this as well?

Collapse
 
lueton profile image
Lueton

Awesome tutorial! Could you do the same with Gitlab CI?

Collapse
 
victornerdunited profile image
Victor Ponce

Why are you saving the artifact if docker build will not use it and build an image itself?

Collapse
 
thakkaryash94 profile image
Yash Thakkar

Because "next-build" will store ".next", "public" folder in CI step, and in dockerfile, we are copying ".next", "public" folders from host to container.

COPY --chown=node:node .next .next
COPY --chown=node:node public public
Enter fullscreen mode Exit fullscreen mode

So we are using these folders in docker build step

Collapse
 
garima2808 profile image
garima2808

Are you open to new job opportunity?