Skip to main content

Deploy a Next.js 14+ App Router Application to CloudBase Run

In one sentence: Use output: 'standalone' in next.config.js to generate a slim build artifact, write a multi-stage Dockerfile, and run tcb cloudrun deploy to deploy the entire Next.js App Router application to CloudBase Run — including SSR, streaming, environment variables, and custom domains.

Estimated time: 30 minutes | Difficulty: Advanced

Applicable Scenarios

This recipe covers the scenario where you build applications with Next.js 14+ and need the deployment to be China-hosted, support domain registration (ICP filing), and have private network connectivity with other cloud resources (database, object storage, Cloud Functions). CloudBase Run fully supports Next.js SSR, streaming responses, and Server Actions.

  • Applicable: Next.js 14 / 15 + App Router + SSR / Streaming / Server Actions
  • Applicable: managing both frontend and backend API from a single repository and deployment workflow
  • Not applicable: pure static sites (SSG) — CloudBase static website hosting is more appropriate
  • Not applicable: demo projects with no server-side logic — GitHub Pages works fine for those

The one-line difference between Cloud Hosting and Cloud Functions: Cloud Functions are for "short tasks, per-request billing, cold starts acceptable"; Cloud Hosting is for "long-running processes, complex runtimes, WebSocket / SSE long connections". A Next.js app with a full SSR lifecycle typically belongs in Cloud Hosting.

Prerequisites

DependencyVersion
Node.js (local dev + image runtime)≥ 18.18 (Next.js 14 minimum requirement)
Next.js14.x or 15.x
Docker (local build verification)latest
@cloudbase/clilatest
A CloudBase Environment with Cloud Hosting enabled

Step 1: Enable standalone output

Add one line to next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
// other config...
};

module.exports = nextConfig;

output: 'standalone' makes next build produce a self-contained artifact under .next/standalone/: a trimmed node_modules (only runtime dependencies actually used) and a server.js entry point. Image size can drop from 1 GB+ to around 200 MB.

After the build, the disk layout looks like:

.next/
├── standalone/ # self-contained runtime (server.js + node_modules)
├── static/ # static assets (JS/CSS chunks) — must be copied separately
└── ...
public/ # your own static assets — must be copied separately

.next/static and public/ are not automatically copied into .next/standalone/. Both directories must be manually COPYed in the Dockerfile — this is the most common mistake. Missing this causes every CSS file and image to return 404 after deployment.

Step 2: Write the multi-stage Dockerfile

Create Dockerfile in the project root:

# ===== Stage 1: deps =====
FROM node:20-alpine AS deps
WORKDIR /app

# Copy only lock files to leverage Docker layer caching
COPY package.json package-lock.json* ./
RUN npm ci

# ===== Stage 2: builder =====
FROM node:20-alpine AS builder
WORKDIR /app

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

# Disable Next.js telemetry (optional, speeds up build)
ENV NEXT_TELEMETRY_DISABLED=1

RUN npm run build

# ===== Stage 3: runner =====
FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME=0.0.0.0

# Run as non-root user
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs

# standalone artifact + static assets
COPY --from=builder /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 3000

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

Key details:

  • HOSTNAME=0.0.0.0 is required. Next.js standalone defaults to listening on localhost, which is unreachable from outside the container.
  • PORT=3000 must match the port number you set when creating the service in CloudBase Run.
  • Use node server.js, not npm start — the standalone entry point is server.js, skipping npm for faster startup.
  • --chown=nextjs:nodejs lets the non-root user read these files; omitting it causes EACCES errors.

Verify locally that the image runs correctly:

docker build -t my-nextjs:local .
docker run -p 3000:3000 -e NODE_ENV=production my-nextjs:local
# Open http://localhost:3000 in a browser to confirm the page loads

Step 3: Deploy to CloudBase Run

Deploy with a single CloudBase CLI command:

tcb login
tcb cloudrun deploy --port 3000

The CLI will ask three things:

  1. Select an Environment ID
  2. Service name (recommend matching the project name, e.g. my-nextjs-app)
  3. Whether to enable public network access (usually yes; otherwise only accessible via private network)

The CLI packages and uploads the current directory, triggering a cloud-side build (CloudBase builds the image from your Dockerfile and deploys it). The entire process typically takes a few minutes.

After deployment, the Console under "Cloud Hosting → Services → your service name" shows a default domain like https://my-nextjs-app-abc123.ap-shanghai.app.tcloudbase.com.

If you prefer not to use the CLI, the Console offers two additional options:

  • "Deploy from local code": upload a zip or folder, cloud-side build.
  • "Deploy from Git repository": connect GitHub / Tencent's internal Git, auto-build on push (recommended for production — CI provides an audit trail).

Step 4: Environment variables

Next.js has two kinds of environment variables, each handled differently in CloudBase Run:

TypeNamingWhen it takes effectHow to configure
Server-side runtime variablesAny nameInjected at container startupCloud Hosting "Service Settings → Environment Variables"
Client-visible variablesNEXT_PUBLIC_*Statically embedded into the client bundle at next build timeMust be present at build time (set in Dockerfile ENV, or injected from cloud at build stage)

NEXT_PUBLIC_* is a Next.js convention: at build time these values are replaced with hardcoded strings in the JS output. This means updating NEXT_PUBLIC_API_URL in the CloudBase Run Console after deployment has no effect — the client bundle is already frozen. Either rebuild, or convert the variable to a runtime fetch call that retrieves config dynamically.

Never put secrets in NEXT_PUBLIC_* — they get bundled into JS files that anyone can read with browser DevTools. See secure-secrets-in-cloud-function for a proper secrets layering approach.

Server-side code (Server Components / Route Handlers / Server Actions) can safely use process.env.SOME_SECRET — that code never enters the client bundle.

To configure environment variables in the CloudBase Run Console:

  1. Go to service details → "Service Settings" → "Version Management" → "New Version"
  2. Add key/value pairs in the "Environment Variables" section
  3. Publish the new version and shift traffic to it

Step 5: Custom domain + HTTPS

The built-in *.app.tcloudbase.com domain works, but production typically requires your own domain.

  1. In the Console go to "Cloud Hosting → Services → your service → Custom Domain" and click "Add Domain".
  2. Enter your domain (e.g. app.example.com); the platform returns a CNAME value.
  3. Add a CNAME record pointing to that value at your DNS provider.
  4. Choose a certificate: "Auto-provision free certificate" (ACME-based, platform-signed) or upload your own.
  5. Wait for the domain status to show "Active" (DNS propagation typically takes a few minutes to a couple of hours).

Once active, requests to https://app.example.com arrive at Next.js with Host: app.example.com. If your code inspects the host header (e.g. for multi-tenant routing), test before and after the custom domain goes live.

Verification

Run these after deployment:

# 1. Health check
curl -I https://your-service.app.tcloudbase.com/
# Expected: HTTP/2 200

# 2. SSR page (confirm server-side rendering works)
curl https://your-service.app.tcloudbase.com/ | grep -o '<title>[^<]*</title>'

# 3. Static assets (confirm public/ was copied correctly)
curl -I https://your-service.app.tcloudbase.com/favicon.ico
# Expected: HTTP/2 200

# 4. _next/static (confirm .next/static was copied correctly)
# In browser DevTools Network tab, verify all /_next/static/chunks/*.js return 200, not 404

If /_next/static/... returns 404, check the Dockerfile for COPY --from=builder /app/.next/static ./.next/static — missing this line is responsible for 99% of such cases.

Common Errors

ErrorCauseFix
Deployment succeeds but returns 503 / container fails to startNext.js inside the image is listening on localhost, port not exposedAdd ENV HOSTNAME=0.0.0.0 to Dockerfile
CSS / fonts / _next/static/*.js all return 404.next/static not COPYed into the runner stageAdd COPY --from=builder /app/.next/static ./.next/static to Dockerfile
/public/*.png images return 404public/ not COPYedAdd COPY --from=builder /app/public ./public
Changed NEXT_PUBLIC_API_URL, restarted service, frontend still has old valueNEXT_PUBLIC_* is injected at build time — changing Console variables does not trigger a rebuildRedeploy, or switch to a runtime fetch /api/config approach
Server Actions fail with Server Action not found in browserSame BUILD_ID is inconsistent across instances (some instances are on the old version)Wait for all instances to roll to the new version, or use CloudBase Run's gradual traffic shifting
Streaming responses appear all at once in the browserAn intermediate layer (CDN / custom domain gateway) has response buffering enabledDisable buffering in custom domain config; if streaming works on localhost but not in production, this is almost always the cause
Image is huge (1 GB+)output: 'standalone' not enabled — full node_modules bundledAdd output: 'standalone' to next.config.js and rebuild
npm ci fails with Cannot find module ...package-lock.json not committed, or local lock is out of sync with package.jsonRun npm install locally and commit the lock file; always use npm ci in the build, never npm install

Build-phase errors are visible with full stack traces under "Cloud Hosting → Service Details → Deployment History → View Logs".

Next Steps

Once the deployment is running, consider:

  • add-vercel-ai-sdk-streaming-chatbot — Add a streaming AI chat interface to Next.js.
  • connect-openai-api-cloud-function — Put the LLM API proxy in a Cloud Function and call it from the Next.js app in CloudBase Run.
  • secure-secrets-in-cloud-function — A secrets layering strategy across dev / staging / prod environments.