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:
- Build Docker image on Gitlab, push to Gitlab registry and deploy to Cloud Run.
- Using Gitlab CI with Cloud Build to complete deployment.
$ npx create-next-app [PROJECT_NAME]
# Run a dev server on local
$ cd [PROJECT_NAME]
$ npm run dev
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 addoutput: "standalone"
innext.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.
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:
- Create a GCP project
- Enable Cloud Run API
- Create a service account and download a JSON key
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.
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
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
- 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
andSecret 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
You can follow previous section to create a project or using exist project.
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.
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"
# 安裝相依套件
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"]