Next.js GitLab CI/CD Docker multi-stage example
This describes an example Next.js project with a GitLab CI/CD pipeline that does the following:
- installs npm packages and builds static assets
- runs ESLint, TypeScript, and Cypress
- builds a Docker image for deployment
- pushes the Docker image to the GitLab Container Registry
This example prepares a Docker image for deployment but doesn't actually deploy it. See an example CI/CD pipeline that deploys to Amazon ECS.
To increase speed and reduce image size, it uses Docker multi-stage builds.
- GitLab repo: https://gitlab.com/saltycrane/next-docker-multi-stage-gitlab-ci-cd-example
- CI/CD Pipelines: https://gitlab.com/saltycrane/next-docker-multi-stage-gitlab-ci-cd-example/-/pipelines
Dockerfile
¶
The Dockerfile defines 3 stages:
- the "builder" stage installs npm packages and builds static assets. It produces artifacts (
/app
and/root/.cache
) that are used by the cypress and deploy stages. It is also used to build an image used to run ESLint and TypeScript. - the "cypress" stage uses a different base image from the "builder" stage and is used to run cypress tests
- the final deploy stage copies the
/app
directory from the "builder" stage and setsNODE_ENV
to "production" and exposes port 3000
ARG BASE_IMAGE=node:14.16-alpine
# ================================================================
# builder stage
# ================================================================
FROM $BASE_IMAGE as builder
ENV NODE_ENV=test
ENV NEXT_TELEMETRY_DISABLED=1
RUN apk add --no-cache bash git
WORKDIR /app
COPY ./package.json ./
COPY ./package-lock.json ./
RUN CI=true npm ci
COPY . ./
RUN NODE_ENV=production npm run build
# ================================================================
# cypress stage
# ================================================================
FROM cypress/base:14.16.0 as cypress
WORKDIR /app
# copy cypress from the builder image
COPY --from=builder /root/.cache /root/.cache/
COPY --from=builder /app ./
ENV NODE_ENV=test
ENV NEXT_TELEMETRY_DISABLED=1
# ================================================================
# final deploy stage
# ================================================================
FROM $BASE_IMAGE
WORKDIR /app
COPY --from=builder /app ./
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
EXPOSE 3000
CMD ["npm", "start"]
.gitlab-ci.yml
¶
- 3 images are built:
test
,cypress
, anddeploy
. Thetest
image is used for running ESLint and TypeScript and is needed forcypress
anddeploy
. Thecypress
image is used for running Cypress. - it uses Docker BuildKit to make caching easier. (With BuildKit, cached layers will be automatically pulled when needed. Without BuildKit, images used for caching need to be explicitly pulled.) For comparison, see this diff adding BuildKit. Note
DOCKER_BUILDKIT
is set to1
to enable BuildKit.
variables:
# enable docker buildkit. Used with `BUILDKIT_INLINE_CACHE=1` below
DOCKER_BUILDKIT: 1
DOCKER_TLS_CERTDIR: "/certs"
IMAGE_TEST: $CI_REGISTRY_IMAGE/test:latest
IMAGE_CYPRESS: $CI_REGISTRY_IMAGE/cypress:latest
IMAGE_DEPLOY: $CI_REGISTRY_IMAGE/deploy:latest
stages:
- build
- misc
- deploy
.base:
image: docker:latest
services:
- docker:dind
before_script:
- docker --version
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
build:builder:
extends: .base
stage: build
script:
- docker build --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from "$IMAGE_TEST" --target builder -t "$IMAGE_TEST" .
- docker push "$IMAGE_TEST"
build:deployimage:
extends: .base
stage: misc
needs: ["build:builder"]
script:
- docker build --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from "$IMAGE_DEPLOY" --cache-from "$IMAGE_TEST" --cache-from "$IMAGE_CYPRESS" -t "$IMAGE_DEPLOY" .
- docker push "$IMAGE_DEPLOY"
test:cypress:
extends: .base
stage: misc
needs: ["build:builder"]
script:
- docker build --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from "$IMAGE_CYPRESS" --cache-from "$IMAGE_TEST" --target cypress -t "$IMAGE_CYPRESS" .
- docker push "$IMAGE_CYPRESS"
- docker run "$IMAGE_CYPRESS" npm run cy:citest
test:eslint:
extends: .base
stage: misc
needs: ["build:builder"]
script:
- docker run "$IMAGE_TEST" npm run eslint
test:typescript:
extends: .base
stage: misc
needs: ["build:builder"]
script:
- docker run "$IMAGE_TEST" npm run tsc
deploy:
stage: deploy
needs: ["build:deployimage", "test:cypress", "test:eslint", "test:typescript"]
script:
- echo "deploy here"
.dockerignore
¶
Adding the .git
directory to .dockerignore
prevented cache invalidation for the COPY . ./
command in the Dockerfile
.
.git
References¶
- https://docs.docker.com/develop/develop-images/multistage-build/
- https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#making-docker-in-docker-builds-faster-with-docker-layer-caching
- https://docs.gitlab.com/ee/ci/directed_acyclic_graph/index.html
- https://docs.gitlab.com/ee/ci/yaml/README.html#needs
- https://testdriven.io/blog/faster-ci-builds-with-docker-cache/
- https://docs.docker.com/engine/reference/commandline/build/#specifying-external-cache-sources
- https://docs.docker.com/develop/develop-images/build_enhancements/
Related posts
- How to use ast-grep with GraphQL — posted 2024-09-24
- Next.js App Router (RSC) projects w/ open source code — posted 2024-07-30
- Next.js Relay GraphQL Pokemon example — posted 2024-05-22
- Example Node.js Passport.js SAML app using OneLogin — posted 2024-05-10
- Aphrodite to CSS Modules codemod — posted 2022-12-09
- Simple codemod example with jscodeshift — posted 2021-05-03