Skip to content

Instantly share code, notes, and snippets.

@andyyou
Created January 16, 2023 06:29
Show Gist options
  • Save andyyou/e04c26a918327094e34e6a3f2a297b47 to your computer and use it in GitHub Desktop.
Save andyyou/e04c26a918327094e34e6a3f2a297b47 to your computer and use it in GitHub Desktop.

2023 Deploy Next.js to Google Cloud Run from Gitlab (Using Gitlab, Cloud Build, Secret Manager)

The core concept in this post is guide you deploy Next.js to Google Cloud Run, and for the CI we will use Gitlab CI. Depends on different services like Cloud Build or build Docker Image on Gitlab there are two sections:

  1. Build Docker image on Gitlab, push to Gitlab registry and deploy to Cloud Run.
  2. Using Gitlab CI with Cloud Build to complete deployment.

Build Docker image on Gitlab, push to Gitlab registry and deploy to Cloud Run.

1. Create a Next.js app

$ npx create-next-app [PROJECT_NAME]

# Run a dev server on local
$ cd [PROJECT_NAME]
$ npm run dev

2. Dockerized Next.js

In root directory of Next.js project

$ touch Dockerfile

The Dockerfile is copy and modify from official example. As time goes on, it may update and you can also read it as reference.

To optimize your production, we use /app/.next/standalone, hence you should add output: "standalone" in next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
output: "standalone",
};

module.exports = nextConfig;

Dockerfile as follow, the example uses multiple stages to reduce size of final image:

# Install dependencies
FROM node:16-alpine AS deps
# Read https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine
# to understand why need libc6-compat
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Use package management tool that you prefer
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

FROM node:16-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Next.js Telemetry
# Read more: https://nextjs.org/telemetry
ENV NEXT_TELEMETRY_DISABLED 1

RUN npm run 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 --from=builder /app/public ./public

# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000

CMD ["node", "server.js"]

Once you complete Dockerfile you can run docker build and docker run command on local

$ docker build -t your_name/nextjs .
$ docker run -p 3000:3000 your_name/nextjs

After you run the docker, you can visit http://localhost:3000 to check a Next.js website. We have dockerized a Next.js project.

3. Setup Google Cloud Service

In first section, we use most of features on Gitlab CI, only Cloud Run on GCP. The deployment of Cloud Run will need a service account key. The steps are:

  1. Create a GCP project
  2. Enable Cloud Run API
  3. Create a service account and download a JSON key

4. Create .gitlab-ci.yml

Before we create .gitlab-ci.yml, we should create a repository on Gitlab, and put "Project ID", "Service Name", "Service Account Key" to CI/CD variables that Gitlab CI can access them.

Sign in on Gitlab > Your project > Left side menu > CI/CD > Variables .

  • GCP_SERVICE_KEY

  • GCP_PROJECT_ID

  • GCP_CLOUD_RUN_SERVICE_NAME

Then we can create .gitlab-ci.yml

image: docker

services:
  - docker:dind

variables:
  DOCKER_HOST: tcp://docker:2375
  DOCKER_DRIVER: overlay2
  CI_IMAGE: $CI_REGISTRY_IMAGE/$GCP_CLOUD_RUN_SERVICE_NAME:$CI_COMMIT_SHORT_SHA
  DEPLOY_IMAGE: gcr.io/$GCP_PROJECT_ID/$GCP_CLOUD_RUN_SERVICE_NAME:$CI_COMMIT_SHORT_SHA

stages:
  - build
  - deploy

build:
  stage: build
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker build -t $CI_IMAGE .
    - docker push $CI_IMAGE

deploy:
  stage: deploy
  image: google/cloud-sdk:slim
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker pull $CI_IMAGE
    - docker tag $CI_IMAGE $DEPLOY_IMAGE
    - echo $GCP_SERVICE_KEY > key.json
    - gcloud auth activate-service-account --key-file key.json
    - gcloud config set project $GCP_PROJECT_ID
    - gcloud auth configure-docker
    - docker push $DEPLOY_IMAGE
    - gcloud run deploy $GCP_CLOUD_RUN_SERVICE_NAME --image $DEPLOY_IMAGE --project $GCP_PROJECT_ID --platform managed --region asia-east1 --allow-unauthenticated
  when: manual
  only:
    - main

You can go to Gitlab > CI/CD > Pipeline to deploy.

5. Option; Environment Variables

If you don't want to commit .env into git repository, and you should handle environment variables on Gitlab or Cloud Build. Next.js need those variables during build time, If you miss them, client side will have no ability to access them.

There is a simple way that save variables as VAR1=a,VAR2=b,VAR3=c format on Gitlab CI/CD variables, and using --build-arg option of Docker to get these data.

We will use NEXT_ENV as variable name on Gitlab and here, change Dockerfile

// ...

FROM node:16-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED 1

ARG NEXT_ENV
RUN echo $NEXT_ENV | tr "," "\n" > .env.production
RUN npm run build

// ...

In .gitlab-ci.yml using --build-arg that value get from Gitlab CI/CD variables

build:
  stage: build
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker build -t $CI_IMAGE . --build-arg "NEXT_ENV=$NEXT_ENV"
    - docker push $CI_IMAGE

Using Gitlab CI with Cloud Build to complete deployment.

The second section, we will no longer build Docker image on Gitlab, instead we use Cloud Build to complete these tasks. Secret Manager handles environment variables as well

1. Setup Google Cloud Servic

  • Create GCP Project
  • Enable Cloud Build
  • Enable Cloud Run API
  • Enable Secret Manager
  • Sign in your GCP console switch to Cloud Build > Settings. Under service account permissions ensure Cloud Run & Service Accounts & Secret Manager Secret Accessor are ENABLED
  • Create a service account with Cloud Build Service Agent and Secret manager Secret Accessor roles. Download JSON key as well or you can visit IAM edit exist account.
  • In GCP console > Secret Manager > Create Secret > Using VAR1=a,VAR2=b,VAR3=c format to create environment variables

2. Create Next.js

You can follow previous section to create a project or using exist project.

3. Gitlab CI/CD, Cloud Build 與 Docker

Overview of the process, we use .gitlab-ci.yml run a Gitlab CI/CD tasks that using gcloud commands, gcloud submit project and cloudbuild.yaml to Cloud Build, then Cloud Build will follow the script in cloudbuild.yaml

  • Access Secret Manager
  • Build docker image
  • Push docker image to registry
  • Deploy to Cloud Run

First, we save variables on Gitlab CI/CD

  • GCP_SERVICE_KEY
  • GCP_PROJECT_ID

Create a .gitlab-ci.yml

image: google/cloud-sdk:slim

stages:
  - deploy

deploy:
  stage: deploy
  script:
    - echo $GCP_SERVICE_KEY > key.json
    - gcloud auth activate-service-account --key-file key.json
    - gcloud config set project $GCP_PROJECT_ID
    - gcloud builds submit . --config=cloudbuild.yaml
  when: manual
  only:
    - main

We only demo deploy stage here, you can change or add more as you want.

4. Cloud Build Script

steps:
  - name: "gcr.io/cloud-builders/docker"
    entrypoint: "bash"
    args:
      [
        "-c",
        "docker build -t gcr.io/$PROJECT_ID/nextjs . --build-arg NEXT_ENV=$$NEXT_ENV",
      ]
    secretEnv: ["NEXT_ENV"]
  - name: "gcr.io/cloud-builders/docker"
    args: ["push", "gcr.io/$PROJECT_ID/nextjs"]
  - name: "gcr.io/cloud-builders/gcloud"
    args:
      [
        "run",
        "deploy",
        "nextjs",
        "--image",
        "gcr.io/$PROJECT_ID/nextjs",
        "--platform",
        "managed",
        "--region",
        "asia-east1",
        "--allow-unauthenticated",
      ]
availableSecrets:
  secretManager:
    - versionName: projects/$PROJECT_ID/secrets/NEXT_ENV/versions/1
      env: "NEXT_ENV"

5. Modify Dockerfile

# 安裝相依套件
FROM node:16-alpine AS deps
# 查看 https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine
# 理解為何需要 libc6-compat
RUN apk add --no-cache libc6-compat
WORKDIR /app

# 根據偏好的套件管理工具安裝套件,您也可以使用 yarn
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

# 僅在需要的時候重新編譯原始碼
FROM node:16-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Next.js 收集一般使用資料 - Telemetry
# 查看更多資料: https://nextjs.org/telemetry
# 如果您需要關閉資料收集可以解開註解
ENV NEXT_TELEMETRY_DISABLED 1

ARG NEXT_ENV
RUN echo $NEXT_ENV | tr "," "\n" > .env.production
RUN npm run build

# 正式環境 Image, 複製全部檔案並執行 Next
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 --from=builder /app/public ./public

# 自動利用輸出軌跡來減小圖像大小
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000

CMD ["node", "server.js"]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment