From Local Next.js App to Production Docker Image on Google Cloud
A Simple Web Developer & Designer
If you’re building a Next.js App Router project with TypeScript, using pnpm and want to deploy it as a Docker container, this guide is for you.
We’ll go from local development → Docker build → versioned image tagging → deployment to Google Cloud Run, all automated with scripts.
Prerequisites
Make sure you have these installed on your system:
Node.js 20+
pnpm (comes with Node via Corepack)
Docker
Google Cloud CLI (gcloud)
Verify your tools:
pnpm --version
docker --version
gcloud --version
If gcloud isn’t installed, follow Google’s Cloud SDK installation guide.
Then log in:
gcloud auth login
Step 1: Local Development Setup
Your Next.js (App Router + TypeScript) project structure should look like this:
my-nextjs-app/
├─ app/
├─ public/
├─ .next/
├─ package.json
├─ pnpm-lock.yaml
├─ tsconfig.json
├─ .eslintrc.json
└─ scripts/
├─ build-docker.sh
└─ deploy-tag.sh
Run locally in development mode
pnpm install
pnpm dev
Open your app at http://localhost:3000 to verify everything works before containerizing it.
Step 2: Create a Production Dockerfile
We’ll use a multi-stage Docker build for performance and security.
Dockerfile
# ================================
# 🏗️ Builder Stage
# ================================
FROM node:20-bullseye-slim AS builder
WORKDIR /app
# Disable telemetry & set production mode
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
# Install build tools
RUN apt-get update && apt-get install -y --no-install-recommends \
git bash curl python3 build-essential libc6-dev \
&& rm -rf /var/lib/apt/lists/*
# Enable pnpm
RUN corepack enable pnpm && corepack prepare pnpm@latest --activate
# Copy package files and install dependencies
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile \
&& pnpm add -D eslint@8.57.0 @next/eslint-plugin-next eslint-plugin-react-hooks@4.6.2
# Copy all project files
COPY . .
# Run lint and TypeScript checks
RUN pnpm run lint
RUN pnpm exec tsc --noEmit
# Build Next.js
RUN pnpm run build
# Remove sensitive files
RUN rm -f .env .env.build
# Prune dev dependencies
RUN pnpm prune --prod
# ================================
# 🚀 Runner Stage (Production)
# ================================
FROM node:20-bullseye-slim AS runner
WORKDIR /app
# Minimal runtime dependencies
RUN apt-get update && apt-get install -y --no-install-recommends libc6-dev \
&& rm -rf /var/lib/apt/lists/* \
&& npm install -g sharp@0.33.0
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV NEXT_SHARP_PATH=/usr/local/lib/node_modules/sharp
ENV HOSTNAME="0.0.0.0"
ENV PORT=3000
ENV NODE_OPTIONS="--unhandled-rejections=strict"
# Create non-root user
RUN groupadd --system nodejs && useradd --system --uid 1001 -g nodejs nextjs
# Copy built assets from builder
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json
USER nextjs
EXPOSE 3000
# Start the app
CMD ["pnpm", "start"]
⚙️ Step 3: Automate Build with build-docker.sh
To simplify the image build process, add this script in your scripts/ folder:
scripts/build-docker.sh
#!/bin/bash
# Usage: ./scripts/build-docker.sh v1.1.0 production
VERSION=$1
ENV=$2
if [ -z "$VERSION" ] || [ -z "$ENV" ]; then
echo "❌ Usage: ./scripts/build-docker.sh <version> <env>"
exit 1
fi
IMAGE_NAME="my-nextjs-app:$VERSION-$ENV"
echo "🏗️ Building Docker image: $IMAGE_NAME ..."
docker build -t $IMAGE_NAME .
echo "✅ Build complete!"
Make it executable:
chmod +x scripts/build-docker.sh
Then build your image:
sudo sh ./scripts/build-docker.sh v1.1.0 production
🔖 Step 4: Push & Deploy with deploy-tag.sh
After building, we’ll tag and deploy the image to Google Cloud.
scripts/deploy-tag.sh
#!/bin/bash
# Usage: ./scripts/deploy-tag.sh v1.1.0 production
VERSION=$1
ENV=$2
if [ -z "$VERSION" ] || [ -z "$ENV" ]; then
echo "❌ Usage: ./scripts/deploy-tag.sh <version> <env>"
exit 1
fi
PROJECT_ID="your-gcp-project-id"
IMAGE_NAME="gcr.io/$PROJECT_ID/my-nextjs-app:$VERSION-$ENV"
echo "🔑 Authenticating with Google Cloud..."
gcloud auth login
gcloud auth configure-docker
echo "🏗️ Building Docker image..."
docker build -t $IMAGE_NAME .
echo "📤 Pushing image to Google Container Registry..."
docker push $IMAGE_NAME
echo "🚀 Deploying to Cloud Run..."
gcloud run deploy my-nextjs-app \
--image $IMAGE_NAME \
--platform managed \
--region us-central1 \
--allow-unauthenticated \
--set-env-vars NODE_ENV=production
echo "✅ Deployment complete! Image: $IMAGE_NAME"
Make it executable:
chmod +x scripts/deploy-tag.sh
🌐 Step 5: Run Everything
Build locally
pnpm build
Build Docker image
sudo sh ./scripts/build-docker.sh v1.1.0 production
Push & Deploy
sudo sh ./scripts/deploy-tag.sh v1.1.0 production
You’ll see output like this:
🔑 Authenticating with Google Cloud...
🏗️ Building Docker image...
📤 Pushing image to registry...
🚀 Deploying to Cloud Run...
✅ Deployment complete! Image: gcr.io/my-project/my-nextjs-app:v1.1.0-production
Now your app is live on a public URL provided by Cloud Run 🎉
Bonus Tips
Always remove
.envfiles from your build context.Use
.dockerignoreto exclude unnecessary files like.next/cacheandnode_modules.Use versioned tags (
v1.1.0,v1.1.1, etc.) for traceable deployments.Add both
build-docker.shanddeploy-tag.shin your CI/CD pipeline for full automation.
Result
You’ve just built a full production-ready pipeline:
Develop locally with pnpm dev
Build and test your app inside Docker
Tag and push versioned images
Deploy seamlessly to Google Cloud Run
This setup ensures consistency, security, and reproducibility across all environments — from your laptop to the cloud.

