Tutorials
GitLab CI/CD for Next.js on Cloud Run
A Next.js app on Cloud Run needs three things from CI: a reproducible Docker build, build-time public env vars baked into the client bundle, and a deploy step that updates the service image without wiping Terraform-managed settings.
This is the pipeline shape we use for the Null World Productions website. It runs on GitLab.com, authenticates to GCP with Workload Identity Federation (OIDC), and deploys only from main.
Pipeline stages
Keep the graph simple:
- test — lint, unit tests, typecheck
- build — Docker image to Artifact Registry
- tag — promote
latestonmain(optional but useful for cache) - deploy —
gcloud run deploywith the new image
Merge requests run test and build. Deploy runs on main after build succeeds.
stages:
- test
- build
- tag
- deploy
Build-time env vars for Next.js
NEXT_PUBLIC_* variables embed in the client JavaScript at build time. Setting them only on Cloud Run at runtime is too late for anything the browser needs (analytics IDs, canonical site URL, feature flags exposed to the client).
Pass them as Docker build args in CI:
build_image:
script:
- |
BUILD_ARGS=""
if [ -n "${NEXT_PUBLIC_GA_ID:-}" ]; then
BUILD_ARGS="${BUILD_ARGS} --build-arg NEXT_PUBLIC_GA_ID=${NEXT_PUBLIC_GA_ID}"
fi
if [ -n "${NEXT_PUBLIC_SITE_URL:-}" ]; then
BUILD_ARGS="${BUILD_ARGS} --build-arg NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL}"
fi
docker build ${BUILD_ARGS} -t "${IMAGE_URI}" -f Dockerfile .
In the Dockerfile:
ARG NEXT_PUBLIC_GA_ID
ARG NEXT_PUBLIC_SITE_URL
ENV NEXT_PUBLIC_GA_ID=$NEXT_PUBLIC_GA_ID
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
Store secrets and environment-scoped values in GitLab CI/CD variables (protected on main, scoped per environment). Never commit them.
Docker layer cache from Artifact Registry
Cold Docker builds on every pipeline waste minutes. Pull the previous image and use --cache-from:
LATEST_IMAGE="${REPO_PATH}:latest"
docker pull "${LATEST_IMAGE}" 2>/dev/null || true
docker build --cache-from "${LATEST_IMAGE}" -t "${IMAGE_URI}" .
docker push "${IMAGE_URI}"
Tag images with the commit SHA for traceability (:${CI_COMMIT_SHORT_SHA}) and maintain a latest tag on main for cache and deploy convenience.
Authenticate without JSON keys
Long-lived GCP service account keys in GitLab variables rot poorly and leak badly. Use GitLab OIDC tokens and Workload Identity Federation.
Each job that touches GCP declares:
id_tokens:
GITLAB_OIDC_TOKEN:
aud: https://gitlab.com
The job exchanges that token for a short-lived GCP access token, then runs gcloud commands. GitLab documents the curl-based exchange; we follow that pattern in deploy jobs.
Required CI variables (names vary by org):
GCP_PROJECT_IDGCP_REGIONGCP_ARTIFACT_REPOGCP_SERVICE_NAMEGCP_WORKLOAD_IDENTITY_POOL/ provider identifiersGCP_SERVICE_ACCOUNT_EMAIL
Terraform usually creates the pool, provider, and IAM bindings. CI only consumes them.
Deploy to Cloud Run
Deploy should change the image (and maybe NODE_ENV), not recreate the entire service config. Infrastructure settings (CPU, memory, min instances, VPC, custom domains) belong in Terraform.
gcloud run deploy "${GCP_SERVICE_NAME}" \
--image "${IMAGE_URI}" \
--region "${GCP_REGION}" \
--platform managed \
--quiet
Verify the image exists in Artifact Registry before deploy. A failed push should fail the pipeline before Cloud Run rolls forward.
Rules example:
deploy_production:
stage: deploy
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
needs:
- build_image
MR vs main behavior
| Event | test | build | deploy |
|---|---|---|---|
| Merge request | yes | yes (verify image builds) | no |
Push to main |
yes | yes | yes |
Building on MRs catches Dockerfile and dependency breaks before merge. Skipping deploy on MRs avoids preview URL complexity unless you add a dedicated review environment.
Failure modes we watch for
- Missing
NEXT_PUBLIC_*at build — site loads but analytics or absolute URLs break silently - Deploy without image verify — Cloud Run keeps old revision; pipeline looks green
- Overwriting Cloud Run flags in deploy — fights Terraform; pick one source of truth for infra
- Unscoped CI variables — staging secrets leaking into production builds
Minimal checklist
- Stages: test → build → deploy
-
NEXT_PUBLIC_*as build args, not runtime-only - OIDC to GCP, no committed keys
- Image tagged by commit SHA
- Deploy gated to
main - Terraform owns service shape; CI owns image promotion
Need help with Cloud Run delivery or GitLab pipeline setup? Contact us.
Read Next
View all postsPostgreSQL for Analytics: Best Practices
Keep OLTP and analytics paths separate, pre-aggregate what dashboards repeat, and index for the filters users actually touch. Patterns we use on BI and ETL projects.
Building Your First Analytics Dashboard
Start with three metrics, one grain, and a schema you can query without heroics. A practical path from spreadsheet exports to a dashboard small teams can maintain.