Jul 13, 2026

Deploy Next.js on GCP: Cloud Run and GKE guide (2026)

Deploy Next.js on Google Cloud Platform with standalone output, Docker, Artifact Registry, Cloud Run, GKE, autoscaling, GitHub Actions, and production-safe caching.

GUIDE14 min readThe Currai team / Engineering

TL;DR: The simplest way to deploy Next.js on GCP is to build a standalone Docker image, store it in Artifact Registry, and run it on Cloud Run. Choose GKE instead when your team already operates Kubernetes or needs pod-level control, sidecars, custom networking, or cluster scheduling. Both options support SSR, App Router streaming, Server Functions, image optimization, and route handlers.

Self-hosting Next.js gives you control over regions, networking, identity, autoscaling, and deployment policy. It also makes your team responsible for cache coordination, secrets, version skew, health checks, rollbacks, security, and production observability.

Cloud Run vs GKE for Next.js

Cloud Run is the default choice for most Next.js applications on Google Cloud. It deploys immutable container revisions, scales instances automatically, and removes the need to manage a Kubernetes control plane. GKE is appropriate when Next.js is one service inside an existing Kubernetes platform or needs controls that Cloud Run does not expose.

RequirementCloud RunGKE
Infrastructure managementFully managedKubernetes cluster and workloads
Scale to zeroBuilt inRequires additional platform design
Per-pod scheduling and sidecarsLimited container-level controlsFull Kubernetes controls
Deployment revisions and traffic splittingBuilt inConfigure with Deployments and routing
Custom cluster networkingManaged networking optionsFull VPC and Kubernetes network control
Operational overheadLowerHigher
Best fitMost standalone Next.js servicesExisting Kubernetes platforms

Google also offers App Engine, Compute Engine, and static hosting options. This guide focuses on container deployments because they preserve the full Next.js server runtime and make the same image portable between Cloud Run and GKE.

What works when self-hosting Next.js on GCP?

According to the official Next.js self-hosting guide, a Node.js or Docker deployment supports App Router streaming, Server Functions, route handlers, image optimization, Proxy, Cache Components, and the after() API.

Static pages can be served through a CDN, while dynamic pages continue to run on the Next.js server. Image optimization runs at request time unless you use a custom loader or external image service.

A static export is different. output: "export" creates HTML, CSS, and JavaScript that can be served without Node.js, but features requiring a server runtime are unavailable. Use standalone output for SSR, authenticated pages, dynamic rendering, server actions, and API routes.

Prerequisites

Before deploying, you need:

  • A Next.js application with a working production build.
  • A Google Cloud project with billing enabled.
  • The Google Cloud CLI authenticated to that project.
  • Artifact Registry, Cloud Build, and Cloud Run APIs enabled.
  • Docker only if you plan to build the image locally.
  • A runtime service account with only the permissions the app needs.

Set the active project and enable the required services:

gcloud config set project PROJECT_ID

gcloud services enable \
  artifactregistry.googleapis.com \
  cloudbuild.googleapis.com \
  run.googleapis.com \
  secretmanager.googleapis.com

Replace placeholders such as PROJECT_ID, REGION, and REPOSITORY throughout the guide. Do not paste credentials into the Dockerfile, repository, image, or build arguments.

Step 1: Enable Next.js standalone output

Next.js output-file tracing identifies the runtime files needed by each page and the production server. Setting output: "standalone" copies those traced files into .next/standalone and creates a minimal server.js.

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "standalone",
  deploymentId: process.env.DEPLOYMENT_VERSION,
};

export default nextConfig;

The standalone directory does not include public or .next/static automatically. The Dockerfile must copy them separately unless they are served from object storage or a CDN.

In a monorepo, tracing starts from the Next.js project directory by default. If the application imports workspace packages or files above that directory, set outputFileTracingRoot to the monorepo root. Build the Docker image from the same root so package manifests and workspace sources exist in the build context.

Step 2: Add a production Next.js Dockerfile

This multi-stage Dockerfile targets a single pnpm application. It installs locked dependencies, builds the standalone server, copies only runtime files, and runs as an unprivileged user.

FROM node:22-slim AS base

ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"

RUN corepack enable

FROM base AS dependencies
WORKDIR /app

COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

FROM base AS builder
WORKDIR /app

COPY --from=dependencies /app/node_modules ./node_modules
COPY . .

ARG DEPLOYMENT_VERSION
ENV DEPLOYMENT_VERSION=$DEPLOYMENT_VERSION

RUN pnpm build

FROM node:22-slim AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV PORT=8080

RUN groupadd --system --gid 1001 nodejs \
  && useradd --system --uid 1001 --gid nodejs nextjs

COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 8080

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

Cloud Run injects a PORT environment variable and requires the ingress container to listen on that port. The standalone Next.js server also needs HOSTNAME=0.0.0.0; binding only to localhost prevents the platform from reaching the process.

For npm or Yarn, replace the pnpm installation steps with the matching locked install command. For a pnpm workspace, also copy pnpm-workspace.yaml, relevant package manifests, and workspace source before installing and building.

Add a .dockerignore

Keep local dependencies, build output, secrets, and Git metadata out of the container context:

.git
.next
node_modules
npm-debug.log*
pnpm-debug.log*
.env*
gha-creds-*.json

If the build intentionally reads a non-secret environment file, adjust the pattern explicitly. Never place server secrets in NEXT_PUBLIC_* variables; Next.js inlines those values into browser JavaScript during next build.

Step 3: Create an Artifact Registry repository

Artifact Registry is Google Cloud's recommended container registry. Older tutorials often use gcr.io, but new deployments should use the REGION-docker.pkg.dev format.

gcloud artifacts repositories create nextjs \
  --repository-format=docker \
  --location=us-central1 \
  --description="Next.js production images"

Configure local Docker authentication when building on your workstation:

gcloud auth configure-docker us-central1-docker.pkg.dev

Use unique release or commit tags instead of updating latest. An immutable tag makes it clear which source produced a Cloud Run revision and makes rollbacks repeatable.

IMAGE=us-central1-docker.pkg.dev/PROJECT_ID/nextjs/web:release-001

Step 4: Build and push the Next.js image

Cloud Build can build the Linux image without relying on the developer's local CPU architecture. Add a cloudbuild.yaml so the immutable release identifier is forwarded to the Docker build argument:

steps:
  - name: gcr.io/cloud-builders/docker
    args:
      - build
      - --build-arg
      - DEPLOYMENT_VERSION=${_DEPLOYMENT_VERSION}
      - --tag
      - ${_IMAGE}
      - .
images:
  - ${_IMAGE}

Submit the build with both substitutions:

gcloud builds submit \
  --config cloudbuild.yaml \
  --substitutions=_IMAGE="$IMAGE",_DEPLOYMENT_VERSION=release-001 \
  .

The Dockerfile uses a build argument, so a direct Docker build can provide it:

docker build \
  --platform linux/amd64 \
  --build-arg DEPLOYMENT_VERSION=release-001 \
  --tag "$IMAGE" \
  .

docker push "$IMAGE"

The explicit platform avoids a common failure when an image is built on an Apple Silicon Mac and deployed to an x86 runtime. Cloud Run also supports Arm in applicable configurations, but the built image must match the selected runtime architecture.

Step 5: Deploy Next.js to Cloud Run

Create a dedicated runtime identity instead of running the application with a broad default service account:

gcloud iam service-accounts create nextjs-runtime \
  --display-name="Next.js Cloud Run runtime"

Grant this identity only the roles required by the application—for example, access to one Secret Manager secret or one Cloud Storage bucket. Deployment permissions belong to the CI/CD identity, not the runtime identity.

Deploy the image:

gcloud run deploy nextjs-web \
  --image "$IMAGE" \
  --region us-central1 \
  --port 8080 \
  --service-account nextjs-runtime@PROJECT_ID.iam.gserviceaccount.com \
  --cpu 1 \
  --memory 512Mi \
  --concurrency 40 \
  --timeout 60s \
  --min-instances 0 \
  --max-instances 20 \
  --allow-unauthenticated

Use --allow-unauthenticated only for a public website. Private dashboards and internal services can require authenticated invocation through Cloud Run IAM or an identity-aware entry point.

Cloud Run resolves the image tag to a digest and creates an immutable revision. Later deployments create new revisions, which can receive all traffic, a small canary percentage, or no traffic until verified.

Configure CPU, memory, and concurrency from measurements

The values above are starting points, not universal production settings. Server-rendered pages, image optimization, large JSON responses, and AI SDKs can have different CPU and memory profiles.

Higher concurrency reduces instance count but makes each process handle more simultaneous renders. If CPU-bound SSR slows under load, reduce concurrency or allocate more CPU. Set maximum instances low enough to protect databases and downstream APIs, then raise it after load tests prove those dependencies can handle the connection volume.

Minimum instances reduce cold starts but keep compute allocated. Measure p50, p95, and p99 latency before paying to keep instances warm.

Step 6: Manage environment variables and secrets

Server-only variables can be supplied when the container starts. Public variables prefixed with NEXT_PUBLIC_ are replaced during the build and cannot be changed by promoting the same image to another environment.

Store sensitive values in Secret Manager and grant the runtime service account access only to the required secrets:

gcloud run services update nextjs-web \
  --region us-central1 \
  --set-secrets="DATABASE_URL=nextjs-database-url:latest"

For non-secret configuration, use --set-env-vars or a declarative service configuration. Avoid placing credentials in build arguments: Docker history and build logs can expose them.

If one image must move through staging and production, read server variables during dynamic rendering. Values used in statically generated pages are fixed at build time even without the NEXT_PUBLIC_ prefix.

Step 7: Add health checks and graceful shutdown

Create a lightweight route that confirms the process can serve requests without running an expensive database or model call:

// app/health/route.ts
export function GET() {
  return Response.json({ status: "ok" });
}

Use platform startup and liveness probes where appropriate. Keep readiness checks separate from deep dependency diagnostics: one slow third-party service should not necessarily restart every healthy Next.js instance.

During termination, let Next.js receive SIGTERM and finish in-flight requests. The official self-hosting guidance recommends a 10-to-30-second drain period so pending after() callbacks can complete. Avoid wrapping server.js in a shell script that swallows signals.

Step 8: Handle caching across Cloud Run instances

Local memory and disk are not a shared durable Next.js cache. When Cloud Run scales to several instances—or GKE runs several pods—each process otherwise has its own cached entries and invalidation state.

For applications that depend on revalidation or cross-request caching:

  1. Configure a custom cache handler backed by shared durable storage.
  2. Set cacheMaxMemorySize: 0 when the design requires disabling the default per-instance memory cache.
  3. Coordinate cache tags through the handler's refreshTags() behavior so revalidateTag() on one instance reaches the others.
  4. Test concurrent requests during deployment and invalidation.

Do not put user-specific dynamic responses behind a public CDN cache. Next.js marks routes using dynamic APIs as private; preserve those cache headers through load balancers and proxies.

Static assets under .next/static use content hashes and are safe candidates for long-lived CDN caching. If assets move to Cloud Storage and Cloud CDN, configure assetPrefix and test the extra DNS, TLS, and cross-origin behavior.

Step 9: Prevent version skew during rollouts

Rolling deployments can temporarily serve old and new Next.js builds at the same time. A browser may load JavaScript from one version and send a Server Function request to another.

Use the same built image for every instance in a revision and set a deployment identifier from the immutable release version:

const nextConfig: NextConfig = {
  output: "standalone",
  deploymentId: process.env.DEPLOYMENT_VERSION,
};

Next.js uses the deployment ID to detect mismatches and fall back to a full page navigation instead of attempting an incompatible client transition.

Server Function closure encryption also needs consistency. Set NEXT_SERVER_ACTIONS_ENCRYPTION_KEY to the same build-time value for all instances built from one release. Store the base64-encoded AES key securely; do not generate a different key in each pod at startup.

Cloud Run traffic splitting can canary a new revision before full rollout. Keep the previous revision available long enough to roll back, and test navigation, prefetching, static assets, and Server Functions while both revisions receive traffic.

Step 10: Preserve Next.js streaming

App Router streaming and Suspense can improve time to first byte by returning a shell while slower server work continues. Verify that each proxy, load balancer, and CDN between the browser and Next.js passes chunked responses without buffering.

Cloud Run supports streaming HTTP responses. A custom reverse proxy or GKE Ingress can still introduce buffering, compression, or timeouts. Test a route with a deliberately delayed Suspense boundary from the public endpoint rather than assuming a successful local test proves end-to-end streaming.

Within the application, start independent server data requests together and use Suspense boundaries around slow regions. Container deployment cannot remove an application-level sequential data-fetching waterfall.

Deploy Next.js to GKE instead

Use GKE when the application belongs in an existing Kubernetes environment or needs pod-level policies, sidecars, custom service meshes, specialized scheduling, or cluster-native service discovery.

The same standalone image can run in a Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nextjs-web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nextjs-web
  template:
    metadata:
      labels:
        app: nextjs-web
    spec:
      serviceAccountName: nextjs-runtime
      containers:
        - name: nextjs
          image: us-central1-docker.pkg.dev/PROJECT_ID/nextjs/web:release-001
          ports:
            - name: http
              containerPort: 8080
          env:
            - name: PORT
              value: "8080"
            - name: HOSTNAME
              value: "0.0.0.0"
          readinessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 15
            periodSeconds: 20
          resources:
            requests:
              cpu: 250m
              memory: 256Mi
            limits:
              cpu: "1"
              memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
  name: nextjs-web
spec:
  selector:
    app: nextjs-web
  ports:
    - name: http
      port: 80
      targetPort: http
  type: ClusterIP

Expose the ClusterIP service through a GKE Gateway or Ingress with managed TLS. Keep the application container private inside the cluster.

Add a HorizontalPodAutoscaler

CPU-based autoscaling requires CPU requests on the container. The HPA uses those requests to calculate utilization:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: nextjs-web
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: nextjs-web
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60

Apply the manifests and inspect the rollout:

kubectl apply -f nextjs.yaml
kubectl rollout status deployment/nextjs-web
kubectl get hpa nextjs-web

GKE does not solve Next.js cache consistency automatically. Use the same shared cache, tag coordination, deployment ID, and Server Function encryption strategy described for multiple Cloud Run instances.

Automate deployment with GitHub Actions

Use GitHub Actions Workload Identity Federation instead of storing a long-lived Google service-account key. The workflow receives a short-lived identity token for the specific repository and job, then authenticates to Google Cloud.

name: Deploy Next.js to Cloud Run

on:
  push:
    branches: [main]

permissions:
  contents: read
  id-token: write

env:
  PROJECT_ID: your-project
  REGION: us-central1
  REPOSITORY: nextjs
  SERVICE: nextjs-web

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: google-github-actions/auth@v3
        with:
          project_id: ${{ env.PROJECT_ID }}
          workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
          service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }}

      - uses: google-github-actions/setup-gcloud@v3

      - name: Configure Artifact Registry
        run: gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet

      - name: Build and push
        env:
          IMAGE: ${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/web:${{ github.sha }}
        run: |
          docker build \
            --build-arg DEPLOYMENT_VERSION="${GITHUB_SHA}" \
            --tag "${IMAGE}" \
            .
          docker push "${IMAGE}"
          echo "IMAGE=${IMAGE}" >> "${GITHUB_ENV}"

      - name: Deploy
        run: |
          gcloud run deploy "${SERVICE}" \
            --image "${IMAGE}" \
            --region "${REGION}" \
            --quiet

Restrict the federated identity to the expected GitHub organization, repository, branch, and workflow. Give the deployment service account only the Cloud Run, Artifact Registry, and service-account impersonation permissions it needs.

Production pipelines should run tests before building, scan the container, deploy a revision without traffic or with a small canary percentage, verify the public health and critical user paths, and then promote traffic.

Common Next.js on GCP deployment problems

Cloud Run says the container failed to start

Confirm that the process listens on the injected PORT and binds to 0.0.0.0. Run the exact production image locally with PORT=8080 and inspect startup logs. Also confirm the image architecture matches the runtime.

CSS, JavaScript, or public images return 404

Standalone output does not copy public or .next/static. Verify both Docker COPY instructions and inspect the final image rather than the builder stage.

Runtime environment changes do not affect the browser

NEXT_PUBLIC_* variables are inlined at build time. Build separate public configuration into each image or load explicitly public runtime configuration from a server endpoint. Never expose a secret to solve this problem.

One instance serves stale data after revalidation

The default cache and tag state are per instance. Configure a shared cache and distributed tag coordination, or avoid cross-request caching for that route.

Server Functions fail during a rollout

Check that every container for the release shares the build-time Server Function encryption key and that deploymentId matches the immutable release. Verify that old assets remain available while old browser sessions exist.

SSR is slow for distant users

Choose a region near the database and primary users before adding more regions. Cross-region database calls can erase the benefit of moving compute closer to the browser. Cache static work, stream slow regions, remove server-side request waterfalls, and measure from real user locations.

Cloud Run creates too many database connections

Instance count multiplied by per-instance concurrency can exceed the database pool. Use a connection-aware proxy or serverless database option, set sensible pool limits, cap Cloud Run maximum instances, and load-test connection behavior.

Frequently asked questions

Can Next.js run on Google Cloud Platform without Vercel?

Yes. Next.js supports self-hosting as a Node.js server or Docker container. Cloud Run, GKE, and other container platforms can run SSR, App Router, route handlers, streaming, image optimization, and Server Functions when configured correctly.

Is Cloud Run or GKE better for Next.js?

Cloud Run is better for most teams because it has lower operational overhead and built-in revision scaling. GKE is better when the team already operates Kubernetes or requires pod-level networking, scheduling, sidecars, policies, or cluster integrations.

Is App Engine a good Next.js hosting option?

App Engine can run Node.js applications, but Cloud Run maps more directly to a portable standalone container and offers a clearer revision-based workflow. Choose App Engine when its application platform model already matches the rest of the system.

Does Cloud Run support Next.js streaming?

Cloud Run supports streaming HTTP responses. Test the complete path when a load balancer, CDN, reverse proxy, or security product sits in front because any layer can buffer or time out the response.

Does Next.js need a shared cache on Cloud Run?

Not every app does. A shared cache is necessary when cached data and tag invalidations must remain consistent across multiple instances. Fully static or deliberately uncached routes may not need one.

How should I deploy static Next.js sites on GCP?

If every route supports static export, use output: "export" and serve the generated files from static hosting or Cloud Storage behind a CDN. Do not ship a Node.js container when the application has no server-runtime requirements.

Observe an AI-enabled Next.js deployment with Currai

Deploying Next.js on GCP tells you whether containers are healthy. Currai shows what happens inside the AI requests those containers serve. Trace each user interaction across model generations, retrieval, tools, and fallbacks; record tokens, model cost, time to first token, end-to-end latency, and errors; and tag traces with the Cloud Run revision, GKE workload, region, and application version.

That context helps separate platform latency from model latency and identify whether a deployment changed AI quality or cost. Currai is not a GCP deployment platform or a replacement for Cloud Monitoring; it is the observability and evaluation layer for the AI workflow inside the application.

Learn how to add OpenTelemetry LLM tracing, measure latency and TTFT, and track token cost. Explore LLM observability, then create a free account to inspect production AI traces from Cloud Run or GKE.

03

Keep going with nearby topics from the Currai blog.