Deploying Next.js on GCP
Source: Dev.to
Why move from Vercel to GCP?
LLM Gateway is a full‑stack application with multiple APIs and frontends. Deploying everything through Vercel added another dashboard, another set of credentials, and another thing to manage. By running on Google Cloud Platform directly we consolidate our infrastructure: APIs, databases, and frontends all live in the same place.
Architecture
All services run on a Kubernetes cluster on GCP. Each component—API, Gateway, UI, Playground, Docs, Admin, and Worker—is deployed as a separate container. Kubernetes handles autoscaling based on resource usage, scaling up during traffic spikes and scaling down when traffic is low.
The build and deployment pipeline is fully automated via GitHub Actions. On every push to main, Docker images for each service are built and pushed to a container registry.
Building a Next.js app in standalone mode
Add the following to next.config.js:
module.exports = {
output: "standalone",
};
This bundles the app into a self‑contained folder with all dependencies.
Dockerfile
# Builder stage
FROM node:20-slim AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
# Runner stage
FROM node:20-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV HOSTNAME="0.0.0.0"
ENV PORT=80
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 80
CMD ["node", "server.js"]
Build and push the image
docker build -t gcr.io/your-project/your-app:latest .
docker push gcr.io/your-project/your-app:latest
Deploy to Cloud Run
gcloud run deploy your-app \
--image gcr.io/your-project/your-app:latest \
--platform managed \
--region us-central1 \
--allow-unauthenticated
Kubernetes Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: your-app
spec:
replicas: 2
selector:
matchLabels:
app: your-app
template:
metadata:
labels:
app: your-app
spec:
containers:
- name: your-app
image: gcr.io/your-project/your-app:latest
ports:
- containerPort: 80
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
Apply with:
kubectl apply -f deployment.yaml
Horizontal Pod Autoscaler
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: your-app
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: your-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
This scales the deployment between 2 and 10 replicas based on CPU utilization.
GitHub Actions workflow (build & push)
Save the following as .github/workflows/build.yml:
name: Build and Push
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
Takeaway
If you’re already on GCP, adding Vercel may be unnecessary. Kubernetes and Cloud Run handle Next.js well, and keeping everything in one place simplifies operations while delivering excellent performance and low latency.