Build NextJS Application Using GitHub Workflow and Docker
Tags: docker, github, nextjs
Published: 2020-11-09 07:09:24

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:

```bash
npx create-next-app
# or
yarn create next-app
```

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](https://nextjs.org/docs/api-reference/create-next-app)

### 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.

```bash
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
```

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](https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions).

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:

```yaml
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
```

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](https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows).
- 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](https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows#manual-events).
- 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](https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets) 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](https://dev-to-uploads.s3.amazonaws.com/i/ctu82o8ig9xuk3xleptd.png)

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](https://dev-to-uploads.s3.amazonaws.com/i/7ocg6k43egzxnypeb4jf.png)

Thank you for reading. Have a great day!

Help Links:

- https://nextjs.org/docs/api-reference/create-next-app
- https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions
- https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows
- https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets