Deploying Next.js on GCP
How we deploy our Next.js apps on Google Cloud Platform without relying on Vercel.

We recently moved our Next.js deployments to Google Cloud Platform. Here's why and how it went.
Why GCP?
LLM Gateway is a full-stack application with multiple APIs and frontends. Deploying everything through Vercel meant adding another tool to our stack—one more dashboard, one more set of credentials, one more thing to manage.
By running on GCP directly, we consolidate our infrastructure. Our APIs, databases, and frontends all live in the same place.
The Setup
All our services run on a Kubernetes cluster on GCP. Each service—API, Gateway, UI, Playground, Docs, Admin, and Worker—is deployed as a separate container. Kubernetes handles autoscaling based on resource usage, so we scale up during traffic spikes and scale down when things are quiet.
The build and deployment pipeline is fully automated via GitHub Actions. On every push to main, we build Docker images for each service and push them to GitHub Container Registry. You can see the workflow here: .github/workflows/images.yml.
For Next.js specifically, we build in standalone mode and package each app into its own container.
Performance
The results surprised us. Performance is excellent—response times are consistently fast, and the infrastructure handles our traffic without issues.
The common concern with self-hosted Next.js is SSR latency from running in a single region. In practice, this hasn't been a problem. The slight increase in latency for users far from our region is negligible compared to the operational simplicity we gained.
How to Self-Host Next.js
Here's how to do it yourself.
Step 1: Enable Standalone Mode
In your next.config.js:
1module.exports = {2 output: "standalone",3};
1module.exports = {2 output: "standalone",3};
This bundles your app into a self-contained folder with all dependencies.
Step 2: Dockerfile
1FROM node:20-slim AS builder2WORKDIR /app3COPY package.json pnpm-lock.yaml ./4RUN npm install -g pnpm && pnpm install --frozen-lockfile5COPY . .6RUN pnpm build78FROM node:20-slim AS runner9WORKDIR /app10ENV NODE_ENV=production11ENV HOSTNAME="0.0.0.0"12ENV PORT=801314COPY --from=builder /app/.next/standalone ./15COPY --from=builder /app/.next/static ./.next/static16COPY --from=builder /app/public ./public1718EXPOSE 8019CMD ["node", "server.js"]
1FROM node:20-slim AS builder2WORKDIR /app3COPY package.json pnpm-lock.yaml ./4RUN npm install -g pnpm && pnpm install --frozen-lockfile5COPY . .6RUN pnpm build78FROM node:20-slim AS runner9WORKDIR /app10ENV NODE_ENV=production11ENV HOSTNAME="0.0.0.0"12ENV PORT=801314COPY --from=builder /app/.next/standalone ./15COPY --from=builder /app/.next/static ./.next/static16COPY --from=builder /app/public ./public1718EXPOSE 8019CMD ["node", "server.js"]
Build and push:
1docker build -t gcr.io/your-project/your-app:latest .2docker push gcr.io/your-project/your-app:latest
1docker build -t gcr.io/your-project/your-app:latest .2docker push gcr.io/your-project/your-app:latest
Step 3a: Deploy to Cloud Run
1gcloud run deploy your-app \2 --image gcr.io/your-project/your-app:latest \3 --platform managed \4 --region us-central1 \5 --allow-unauthenticated
1gcloud run deploy your-app \2 --image gcr.io/your-project/your-app:latest \3 --platform managed \4 --region us-central1 \5 --allow-unauthenticated
Step 3b: Deploy to Kubernetes
1apiVersion: apps/v12kind: Deployment3metadata:4 name: your-app5spec:6 replicas: 27 selector:8 matchLabels:9 app: your-app10 template:11 metadata:12 labels:13 app: your-app14 spec:15 containers:16 - name: your-app17 image: gcr.io/your-project/your-app:latest18 ports:19 - containerPort: 8020 resources:21 requests:22 memory: "256Mi"23 cpu: "100m"24 limits:25 memory: "512Mi"26 cpu: "500m"
1apiVersion: apps/v12kind: Deployment3metadata:4 name: your-app5spec:6 replicas: 27 selector:8 matchLabels:9 app: your-app10 template:11 metadata:12 labels:13 app: your-app14 spec:15 containers:16 - name: your-app17 image: gcr.io/your-project/your-app:latest18 ports:19 - containerPort: 8020 resources:21 requests:22 memory: "256Mi"23 cpu: "100m"24 limits:25 memory: "512Mi"26 cpu: "500m"
Apply with kubectl apply -f deployment.yaml.
Bonus: Autoscaling
For automatic scaling based on CPU usage, add a HorizontalPodAutoscaler:
1apiVersion: autoscaling/v22kind: HorizontalPodAutoscaler3metadata:4 name: your-app5spec:6 scaleTargetRef:7 apiVersion: apps/v18 kind: Deployment9 name: your-app10 minReplicas: 211 maxReplicas: 1012 metrics:13 - type: Resource14 resource:15 name: cpu16 target:17 type: Utilization18 averageUtilization: 70
1apiVersion: autoscaling/v22kind: HorizontalPodAutoscaler3metadata:4 name: your-app5spec:6 scaleTargetRef:7 apiVersion: apps/v18 kind: Deployment9 name: your-app10 minReplicas: 211 maxReplicas: 1012 metrics:13 - type: Resource14 resource:15 name: cpu16 target:17 type: Utilization18 averageUtilization: 70
This scales your deployment between 2 and 10 replicas based on CPU utilization.
Bonus: CI/CD with GitHub Actions
Automate builds with GitHub Actions. This workflow builds and pushes to GitHub Container Registry on every push to main:
1name: Build and Push23on:4 push:5 branches: [main]67jobs:8 build:9 runs-on: ubuntu-latest10 permissions:11 contents: read12 packages: write13 steps:14 - uses: actions/checkout@v41516 - uses: docker/setup-buildx-action@v31718 - uses: docker/login-action@v319 with:20 registry: ghcr.io21 username: ${{ github.actor }}22 password: ${{ secrets.GITHUB_TOKEN }}2324 - uses: docker/build-push-action@v625 with:26 context: .27 push: true28 tags: ghcr.io/${{ github.repository }}:latest29 cache-from: type=gha30 cache-to: type=gha,mode=max
1name: Build and Push23on:4 push:5 branches: [main]67jobs:8 build:9 runs-on: ubuntu-latest10 permissions:11 contents: read12 packages: write13 steps:14 - uses: actions/checkout@v41516 - uses: docker/setup-buildx-action@v31718 - uses: docker/login-action@v319 with:20 registry: ghcr.io21 username: ${{ github.actor }}22 password: ${{ secrets.GITHUB_TOKEN }}2324 - uses: docker/build-push-action@v625 with:26 context: .27 push: true28 tags: ghcr.io/${{ github.repository }}:latest29 cache-from: type=gha30 cache-to: type=gha,mode=max
Save as .github/workflows/build.yml.
Takeaway
If you're already on GCP and considering whether to add Vercel to your stack, you might not need to. Kubernetes and Cloud Run handle Next.js well, and keeping everything in one place makes operations simpler.