Deploy a Next.js 14+ App Router Application to CloudBase Run
In one sentence: Use
output: 'standalone'innext.config.jsto generate a slim build artifact, write a multi-stage Dockerfile, and runtcb cloudrun deployto 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
| Dependency | Version |
|---|---|
| Node.js (local dev + image runtime) | ≥ 18.18 (Next.js 14 minimum requirement) |
| Next.js | 14.x or 15.x |
| Docker (local build verification) | latest |
@cloudbase/cli | latest |
| 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 /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 /app/public ./public
COPY /app/.next/standalone ./
COPY /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
Key details:
HOSTNAME=0.0.0.0is required. Next.js standalone defaults to listening onlocalhost, which is unreachable from outside the container.PORT=3000must match the port number you set when creating the service in CloudBase Run.- Use
node server.js, notnpm start— the standalone entry point isserver.js, skipping npm for faster startup. --chown=nextjs:nodejslets the non-root user read these files; omitting it causesEACCESerrors.
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:
- Select an Environment ID
- Service name (recommend matching the project name, e.g.
my-nextjs-app) - 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:
| Type | Naming | When it takes effect | How to configure |
|---|---|---|---|
| Server-side runtime variables | Any name | Injected at container startup | Cloud Hosting "Service Settings → Environment Variables" |
| Client-visible variables | NEXT_PUBLIC_* | Statically embedded into the client bundle at next build time | Must 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:
- Go to service details → "Service Settings" → "Version Management" → "New Version"
- Add key/value pairs in the "Environment Variables" section
- 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.
- In the Console go to "Cloud Hosting → Services → your service → Custom Domain" and click "Add Domain".
- Enter your domain (e.g.
app.example.com); the platform returns a CNAME value. - Add a CNAME record pointing to that value at your DNS provider.
- Choose a certificate: "Auto-provision free certificate" (ACME-based, platform-signed) or upload your own.
- 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
| Error | Cause | Fix |
|---|---|---|
| Deployment succeeds but returns 503 / container fails to start | Next.js inside the image is listening on localhost, port not exposed | Add ENV HOSTNAME=0.0.0.0 to Dockerfile |
CSS / fonts / _next/static/*.js all return 404 | .next/static not COPYed into the runner stage | Add COPY --from=builder /app/.next/static ./.next/static to Dockerfile |
/public/*.png images return 404 | public/ not COPYed | Add COPY --from=builder /app/public ./public |
Changed NEXT_PUBLIC_API_URL, restarted service, frontend still has old value | NEXT_PUBLIC_* is injected at build time — changing Console variables does not trigger a rebuild | Redeploy, or switch to a runtime fetch /api/config approach |
Server Actions fail with Server Action not found in browser | Same 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 browser | An intermediate layer (CDN / custom domain gateway) has response buffering enabled | Disable 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 bundled | Add 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.json | Run 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".
Related Documentation
- Next.js Framework Integration — Official quick-start guide (this recipe is the end-to-end version)
- Deploy via CLI —
tcb cloudrun deploydetailed parameters - Deploy via Git — Connect GitHub / Tencent's internal Git for auto-deploy
- Custom Domains — Full domain + certificate configuration
- Environment Variables — Service-level env settings
- Dockerfile Guide — Platform requirements for Dockerfiles
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.