Deploy Next.js lên Production: Từ git push đến monitoring thực tế
Bài cuối series React → Next.js: tất cả những gì cần biết để đưa app lên production an toàn, từ Vercel đến self-hosted Docker.
Nguyễn Nhật Long
@nguyennhatlong1303
Đây là bài thứ 12 cũng là bài cuối trong series từ React đến Next.js. Sau 11 bài đào sâu vào routing, data fetching, Server Components, authentication... giờ là lúc nói về thứ quan trọng nhất: đưa cái app đó lên production và đảm bảo nó chạy ổn định.
Mình sẽ không nói kiểu "deploy là bước cuối cùng" vì thực ra deploy chỉ là bắt đầu. Phần khó hơn là monitor, debug production issues, và đảm bảo performance không tụt dốc sau khi có traffic thật.
Vercel Zero Config nhưng không có nghĩa là Zero Knowledge
Nếu bạn chưa từng dùng Vercel, trải nghiệm lần đầu sẽ khá ấn tượng. Connect GitHub repo, push code, xong app live. Không cần config gì cả.
Nhưng để dùng Vercel đúng cách trong môi trường production thật sự, có vài thứ bạn cần nắm:
Preview Deployments là tính năng mình thấy cực kỳ hữu ích khi làm việc nhóm. Mỗi PR tự động có một preview URL riêng kiểu your-app-git-feature-xyz-yourteam.vercel.app. QA có thể test trực tiếp trên đó mà không cần pull code về local. Reviewer cũng có thể click vào xem UI thay vì chỉ đọc code. Workflow này tiết kiệm khá nhiều thời gian.
Environment Variables trên Vercel có 3 scope: Production, Preview, và Development. Đừng nhét tất cả vào Production rồi thôi hãy tách biệt rõ ràng. Database staging không nên dùng chung với production, đây là điều hiển nhiên nhưng mình vẫn thấy nhiều team làm sai.
Edge Functions middleware của Next.js chạy trên Vercel Edge Network, tức là chạy gần người dùng nhất có thể (CDN edge nodes). Cold start gần như bằng 0. Đây là lý do middleware thường được dùng cho auth checks, redirects, A/B testing những thứ cần response nhanh nhưng không cần full Node.js environment.
represented as connected circles on a world map. Clean flat design, dark background, blue and white accent colors.)
Self-hosted với Docker Khi bạn cần kiểm soát nhiều hơn
Vercel tuyệt vời, nhưng không phải lúc nào cũng phù hợp. Một số công ty có yêu cầu data residency (dữ liệu phải ở trong nước), hoặc đơn giản là budget không cho phép scale trên Vercel khi traffic lớn. Lúc đó self-hosted là lựa chọn.
Bước đầu tiên là config next.config.ts:
1// next.config.ts2const nextConfig = {3 output: 'standalone',4 // standalone mode tạo ra một folder chứa tất cả5 // dependencies cần thiết, không cần node_modules đầy đủ6};78export default nextConfig;
Với output: 'standalone', Next.js sẽ bundle chỉ những gì cần thiết để chạy app rất tiện cho Docker image nhỏ gọn.
Dockerfile mình hay dùng theo pattern multi-stage build:
1# Stage 1: Dependencies2FROM node:20-alpine AS deps3WORKDIR /app4COPY package.json package-lock.json ./5RUN npm ci --only=production67# Stage 2: Builder8FROM node:20-alpine AS builder9WORKDIR /app10COPY --from=deps /app/node_modules ./node_modules11COPY . .12RUN npm run build1314# Stage 3: Runner15FROM node:20-alpine AS runner16WORKDIR /app17ENV NODE_ENV=production1819COPY --from=builder /app/public ./public20COPY --from=builder /app/.next/standalone ./21COPY --from=builder /app/.next/static ./.next/static2223EXPOSE 300024CMD ["node", "server.js"]
Multi-stage build giúp image cuối cùng không chứa dev dependencies, source code gốc, hay các build artifacts không cần thiết. Image size giảm đáng kể.
Rồi docker-compose.yml để orchestrate toàn bộ stack:
1services:2 app:3 build: .4 ports:5 - "3000:3000"6 environment:7 - DATABASE_URL=postgresql://user:pass@db:5432/myapp8 - REDIS_URL=redis://cache:63799 depends_on:10 - db11 - cache1213 db:14 image: postgres:16-alpine15 volumes:16 - postgres_data:/var/lib/postgresql/data17 environment:18 POSTGRES_DB: myapp19 POSTGRES_USER: user20 POSTGRES_PASSWORD: pass2122 cache:23 image: redis:7-alpine24 volumes:25 - redis_data:/data2627 nginx:28 image: nginx:alpine29 ports:30 - "80:80"31 - "443:443"32 volumes:33 - ./nginx.conf:/etc/nginx/nginx.conf34 - ./certs:/etc/nginx/certs35 depends_on:36 - app3738volumes:39 postgres_data:40 redis_data:
Về reverse proxy, mình thấy Caddy ngày càng được ưa dùng hơn Nginx vì tự động handle SSL với Let's Encrypt mà không cần config thêm gì. Nhưng nếu team đã quen Nginx thì cứ dùng Nginx không có gì sai cả.
Environment Variables Đừng để secret lọt ra client
Đây là thứ mình thấy nhiều người mới bị nhầm. Next.js có quy ước rõ ràng:
Ví dụ:
| Prefix | Accessible ở | Use case |
|---|---|---|
| `NEXT_PUBLIC_*` | Client + Server | API public URL, analytics ID, feature flags |
| Không có prefix | Server only | Database URL, secret keys, API private keys |
1# .env.local (gitignored KHÔNG commit file này)2DATABASE_URL=postgresql://localhost:5432/myapp_dev3JWT_SECRET=your-super-secret-key4STRIPE_SECRET_KEY=sk_test_...56# Cái này expose ra client bundle ai cũng đọc được7NEXT_PUBLIC_API_URL=https://api.yourapp.com8NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
Một lỗi mình hay thấy: đặt tên NEXT_PUBLIC_DATABASE_URL rồi wonder tại sao database bị attack. Bất cứ thứ gì có prefix NEXT_PUBLIC_ đều nằm trong JavaScript bundle gửi về browser bất kỳ ai cũng có thể đọc được bằng DevTools.
Edge vs Node.js Runtime Chọn đúng cho đúng việc
Next.js cho phép bạn chọn runtime cho từng route. Đây là trade-off cần hiểu rõ:
Thực tế mình hay dùng Edge cho middleware (auth check, redirect) và Node.js cho API routes có database query. Đừng cố nhét database query vào Edge Runtime hầu hết database clients không support môi trường đó.
| Edge Runtime | Node.js Runtime | |
|---|---|---|
| Cold start | ~0ms | 100-500ms |
| Memory | Giới hạn (128MB) | Đủ dùng |
| Node.js APIs | Không hỗ trợ | Full support |
| Database clients | Hầu hết không dùng được | Bình thường |
| Phù hợp cho | Auth, redirects, geo-routing | DB queries, file I/O, heavy processing |
1// middleware.ts chạy trên Edge, perfect cho auth check2export const config = {3 runtime: 'edge',4 matcher: ['/dashboard/:path*', '/api/protected/:path*'],5};67// app/api/users/route.ts cần Node.js vì dùng Prisma8export const runtime = 'nodejs'; // hoặc không khai báo, mặc định là nodejs
Core Web Vitals và tại sao bạn nên quan tâm từ sớm
Google dùng Core Web Vitals để rank trang web. Quan trọng hơn, nó phản ánh trải nghiệm thật của người dùng. Ba metric chính:
- LCP (Largest Contentful Paint): Thời gian render element lớn nhất trong viewport. Target < 2.5s. Thường bị ảnh hưởng bởi hero image hoặc main content block.
- INP (Interaction to Next Paint): Thay thế FID từ 2024, đo responsiveness khi user interact. Target < 200ms.
- CLS (Cumulative Layout Shift): Layout bị nhảy lung tung khi load. Target < 0.1. Thủ phạm phổ biến: image không có width/height, font swap, dynamic content inject.
Next.js giúp bạn khá nhiều ở đây. next/image tự động:
- Lazy load images ngoài viewport
- Convert sang WebP/AVIF
- Yêu cầu khai báo
widthvàheight(fix CLS) - Serve đúng size theo device
1import Image from 'next/image';23// Đúng next/image handle hết4<Image5 src="/hero.jpg"6 alt="Hero image"7 width={1200}8 height={600}9 priority // dùng cho above-the-fold images, tắt lazy load10/>1112// Sai dùng <img> thường, mất hết optimization13<img src="/hero.jpg" alt="Hero image" />
Về bundle size, mình hay chạy @next/bundle-analyzer để xem cái gì đang phình to:
1npm install @next/bundle-analyzer
1// next.config.ts2import bundleAnalyzer from '@next/bundle-analyzer';34const withBundleAnalyzer = bundleAnalyzer({5 enabled: process.env.ANALYZE === 'true',6});78export default withBundleAnalyzer(nextConfig);
1ANALYZE=true npm run build
Nó sẽ mở browser với treemap visualization cho thấy package nào chiếm bao nhiêu KB. Theo kinh nghiệm của mình, thủ phạm thường gặp nhất là moment.js (thay bằng date-fns), lodash import nguyên cả library thay vì tree-shake, và các icon library import cả bộ thay vì từng icon.
Sentry + Vercel Analytics Monitoring thực tế
Error tracking và performance monitoring là hai thứ khác nhau, cần cả hai.
Sentry cho error tracking:
1npm install @sentry/nextjs2npx @sentry/wizard@latest -i nextjs
Sentry tự động capture unhandled exceptions, nhưng bạn cũng nên manually capture các business logic errors:
1import * as Sentry from '@sentry/nextjs';23try {4 await processPayment(orderId);5} catch (error) {6 Sentry.captureException(error, {7 tags: { orderId, userId },8 level: 'error',9 });10 throw error;11}
Vercel Analytics cho real user metrics nó tự động collect Core Web Vitals từ actual users, không phải synthetic tests. Bạn sẽ thấy sự khác biệt khá rõ giữa Lighthouse score local và real user data, đặc biệt trên mobile và connection chậm.
Production Checklist Cái này in ra dán lên màn hình
Mình tổng hợp lại checklist trước khi go-live. Không phải để tick cho xong, mà mỗi item đều có lý do cụ thể:
1□ TypeScript strict mode bật, không có `any` tùy tiện2□ Environment variables đã verify đúng scope (không để secret ra NEXT_PUBLIC_)3□ Error boundaries bao quanh các page/section quan trọng4□ Metadata và OG tags đầy đủ trên mọi page5□ Tất cả ảnh dùng next/image với width/height khai báo đúng6□ Loading states (loading.tsx) cho các route có data fetching7□ Custom 404 và error page8□ Core Web Vitals đạt target (LCP < 2.5s, INP < 200ms, CLS < 0.1)9□ Security headers đã config (X-Frame-Options, CSP, etc.)10□ Database connection pooling (Prisma, pg-pool, hoặc PgBouncer)11□ Logging structured và có thể query được12□ Sentry (hoặc tương đương) đã setup và test
Về security headers, Next.js cho phép config trong next.config.ts:
1const securityHeaders = [2 { key: 'X-Frame-Options', value: 'SAMEORIGIN' },3 { key: 'X-Content-Type-Options', value: 'nosniff' },4 { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },5 {6 key: 'Content-Security-Policy',7 value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'",8 },9];1011export default {12 async headers() {13 return [14 {15 source: '/(.*)',16 headers: securityHeaders,17 },18 ];19 },20};
Database connection pooling hay bị bỏ qua cho đến khi app bị crash vì quá nhiều connections. Với Prisma trên serverless/edge, bạn cần Prisma Accelerate hoặc PgBouncer. Với self-hosted Node.js thì Prisma tự handle pool khá ổn, nhưng vẫn nên explicit config pool size.
Series 12 bài đến đây là xong. Từ React cơ bản, qua Next.js App Router, Server Components, data fetching patterns, authentication, đến deployment và monitoring. Mình cố gắng viết theo hướng thực chiến những thứ mình thật sự dùng trong dự án, không phải chỉ copy docs lại.
Nếu bạn có câu hỏi về bất kỳ phần nào, hoặc muốn mình đào sâu hơn vào một topic cụ thể (ví dụ deployment với Kubernetes, hay monitoring nâng cao với OpenTelemetry), cứ drop comment. Mình đọc hết.
Nguyễn Nhật Long
@nguyennhatlong1303Nguyễn Nhật Long is a Senior Frontend Engineer and Frontend Team Leader with 7 years of experience building real-time fintech platforms. Specializing in React, Next.js, TypeScript, and React Native, shipping 10+ products across Web, Mobile, Telegram Mini-Apps, and Web3.
Thấy hay? Chia sẻ cho bạn bè!
