Hướng Dẫn Deploy Next.js Lên VPS Bằng Coolify Chi Tiết A-Z
Deploy Next.js lên VPS bằng Coolify: quy trình mình đang chạy và mấy lỗi hay dính nhất
Sau vài lần dùng Vercel cho side project rồi nhận hoá đơn tăng dần theo băng thông, mình chuyển qua tự host Next.js trên VPS bằng Coolify. Không phải vì Vercel tệ, mà vì mình muốn kiểm soát tài nguyên và chi phí rõ ràng hơn. Bài này ghi lại đúng những gì mình làm để deploy được, kèm mấy lỗi đã dính thật trong lúc build.
Chuẩn bị trước khi bắt đầu
Trước khi mở Coolify lên, cần có sẵn:
- Một VPS chạy Ubuntu (khuyến nghị 22.04 trở lên), có quyền root hoặc sudo.
- Domain đã trỏ được DNS, ít nhất một subdomain để gắn cho app.
- Repo Next.js đã push lên GitHub.
- Docker chưa cần cài tay - script cài Coolify sẽ tự lo phần này nếu máy chưa có.
Nếu VPS còn trống hoàn toàn, chỉ cần một bản Ubuntu sạch là đủ, không cần cài Node hay Nginx trước, vì toàn bộ phần build và serve sẽ chạy trong container do Coolify quản lý.
Cài Coolify lên VPS
SSH vào VPS rồi chạy lệnh cài chính thức từ Coolify:
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash

Script này cài Docker (nếu chưa có), kéo image Coolify, và khởi tạo dashboard. Sau khi chạy xong, truy cập:
http://<ip-vps-cua-ban>:8000

Lần đầu vào sẽ được yêu cầu tạo tài khoản admin. Từ bản Coolify v4, dashboard mặc định chạy ở port 8000, sau này khi gắn domain cho chính Coolify thì có thể đổi qua HTTPS port 443 như bình thường.
Kết nối GitHub repo vào Coolify
Coolify v4 dùng GitHub App để lấy quyền truy cập repo, thay vì chỉ nhập access token thô như một số bản cũ. Vào Sources (hoặc Settings > Sources tuỳ version), chọn thêm GitHub App, làm theo hướng dẫn để cài app đó lên tài khoản hoặc organization chứa repo. Sau bước này Coolify mới thấy được danh sách repo private của bạn.
Nếu repo là public, có thể bỏ qua bước GitHub App và dán trực tiếp URL repo khi tạo Application, nhưng như vậy sẽ không có webhook tự deploy - phần này nói ở cuối bài.

Tạo Application và chọn build pack
Vào Projects > New Resource > Application, chọn repo và branch muốn deploy. Coolify sẽ hỏi build pack:
- Nixpacks - mặc định, tự nhận diện project Next.js qua
package.json, tự chạynpm install,npm run build,npm start(hoặc script tương ứng theo package manager bạn dùng). - Dockerfile - dùng khi cần kiểm soát chi tiết hơn: multi-stage build, cài thêm dependency hệ thống mà Nixpacks không có sẵn, hoặc muốn build image nhẹ hơn với Next.js standalone output.
Với đa số project Next.js thông thường, Nixpacks chạy ổn, mình để mặc định. Chỉ khi project cần thêm lib hệ thống (ví dụ để chạy Puppeteer, hoặc cần build với output: 'standalone' và copy thủ công node_modules tối giản) mình mới chuyển sang Dockerfile, dạng đơn giản như:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY /app/.next/standalone ./
COPY /app/.next/static ./.next/static
COPY /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
Dockerfile này chỉ chạy được khi next.config.js có output: 'standalone', nếu không copy thiếu file, container sẽ báo lỗi missing module khi start.
Cấu hình biến môi trường (chỗ dễ nhầm nhất)
Đây là phần mình bị dính lỗi ngay lần deploy đầu tiên. Next.js phân biệt rõ hai loại biến:
- Biến bắt đầu bằng
NEXT_PUBLIC_được Next.js inline trực tiếp vào bundle client trong lúcnext build. Nghĩa là nó phải có mặt tại thời điểm build, không phải lúc container chạy. - Biến thường (không có
NEXT_PUBLIC_) chỉ được đọc ở server, có thể set ở runtime bình thường.
Trong Coolify, mỗi biến môi trường có thể đánh dấu là Build Variable (available at build time) hoặc chỉ runtime. Lần đầu mình set NEXT_PUBLIC_API_URL như một biến runtime bình thường, kết quả là app build xong, chạy lên, nhưng phía client gọi API vào undefined. Debug mãi mới nhận ra: lúc build, Coolify chưa "thấy" biến đó nên Next.js đã inline sẵn undefined vào bundle, set runtime sau đó không sửa lại được vì file JS tĩnh đã build xong rồi.
Cách đúng:
# Biến này BẮT BUỘC phải tick "available at buildtime" trong Coolify
NEXT_PUBLIC_API_URL=https://api.example.com
# Biến server-side, chỉ cần runtime, không cần buildtime
DATABASE_URL=postgres://user:pass@postgres-service:5432/dbname
NEXTAUTH_SECRET=your-secret-here
Quy tắc mình tự đặt cho mình từ sau lần đó: cứ thấy prefix NEXT_PUBLIC_ là tick buildtime, không cần suy nghĩ thêm.
Gắn domain và SSL
Vào tab Domains của Application, nhập domain hoặc subdomain (ví dụ app.domain.com). Trước đó DNS phải trỏ A record về đúng IP VPS. Coolify dùng Traefik làm reverse proxy đứng trước tất cả app, và Traefik tự động xin chứng chỉ Let's Encrypt ngay khi phát hiện domain trỏ đúng - không cần đụng tới Certbot hay cấu hình Nginx tay.
Điểm cần chú ý: nếu DNS chưa propagate xong mà đã bấm deploy, Traefik sẽ fail lúc xin cert, lúc đó chỉ cần đợi DNS trỏ đúng rồi trigger lại deploy (không cần build lại từ đầu, chỉ cần redeploy).

Deploy lần đầu và xem log
Bấm Deploy, Coolify sẽ kéo code, build theo build pack đã chọn, rồi start container. Log build hiện trực tiếp trên dashboard theo thời gian thực - chỗ này quan trọng, vì gần như mọi lỗi ở phần sau đều lộ ra trong log lúc build hoặc lúc container start, không phải lỗi "mơ hồ" như một số PaaS khác giấu log.
Sau khi container chạy, kiểm tra thêm log runtime (tab Logs, riêng với build log) để chắc app không crash-loop ngay sau khi start.
Xử lý lỗi thường gặp
Build fail với Nixpacks
Thường gặp nhất là do thiếu lockfile (package-lock.json hoặc pnpm-lock.yaml không được commit), khiến Nixpacks không xác định đúng package manager và version. Kiểm tra lại repo có commit đúng lockfile chưa trước khi nghi ngờ Coolify.
Trường hợp khác: project dùng version Node mới hơn version Nixpacks tự chọn mặc định. Có thể ép version bằng cách thêm file .nvmrc hoặc set biến NIXPACKS_NODE_VERSION trong phần cấu hình build của Coolify.
Build bị OOM (out of memory)
Đây là lỗi đau nhất với VPS RAM thấp. Next.js build (đặc biệt bước webpack compile + static generation) có thể ngốn RAM vượt quá VPS 1-2GB, container build bị kill giữa chừng, log thường dừng đột ngột không có traceback rõ ràng - exit code kiểu 137 là dấu hiệu bị OOM kill.
Ba cách mình từng dùng, theo thứ tự ưu tiên:
- Thêm swap cho VPS. Không thay được RAM thật nhưng đủ để build không bị kill giữa chừng, đổi lại build chậm hơn vì phải swap ra đĩa.
- Set biến buildtime
NODE_OPTIONS=--max-old-space-size=2048(chỉnh số theo RAM thực tế của VPS) để giới hạn heap Node, tránh process nuốt hết RAM rồi kernel OOM-kill toàn container. - Nếu VPS hỗ trợ resize linh hoạt (tăng RAM tạm thời), nâng cấu hình lên trong lúc build lần đầu rồi hạ lại sau - cách này hiệu quả nhất với site có bundle lớn nhưng chạy production không cần nhiều RAM.
App "unreachable" sau một thời gian chạy
Vài lần app chạy ổn được một lúc rồi ngưng phản hồi. Nguyên nhân mình gặp là container bị OOM-kill ở runtime (không phải lúc build) do RAM VPS không đủ cho cả Coolify, Traefik và app cùng lúc, đặc biệt khi có thêm database chạy chung máy. Check bằng docker stats hoặc log container để thấy có bị restart lặp không, rồi cân đối lại RAM hoặc tách database ra VPS riêng nếu cần.
Nhầm biến môi trường
Đã nói ở phần trên - dấu hiệu: dữ liệu client-side luôn undefined hoặc trỏ nhầm domain, dù đã set đúng giá trị trong Coolify. Kiểm tra lại cờ buildtime trước khi nghi ngờ code.
Kết nối database qua internal hostname
Nếu Postgres hoặc MySQL cũng được deploy như một resource trong Coolify, nó nằm trong cùng Docker network với app. Không cần và không nên dùng IP public của VPS để kết nối - dùng internal hostname (tên service Coolify đặt cho database) sẽ nhanh hơn và không expose port database ra ngoài:
DATABASE_URL=postgres://user:password@postgres-service-name:5432/mydb
Tên service này xem trong tab của resource database trên Coolify, không phải domain public.
Tối ưu ảnh Next.js
Nếu dùng next/image với image optimization mặc định, một số base image Alpine thiếu thư viện hệ thống mà sharp cần, dẫn tới lỗi lúc runtime khi Next.js cố resize ảnh. Cách xử lý nhanh nếu không muốn debug native dependency: set images: { unoptimized: true } trong next.config.js để bỏ qua optimizer tích hợp, đổi lại ảnh không được resize/convert tự động nữa.
Bảng cấu hình VPS theo nhu cầu
| Nhu cầu | RAM tối thiểu | Ghi chú |
|---|---|---|
| Site nhỏ, không database | 2GB | Đủ chạy Coolify + 1 app Next.js nhẹ, build có thể chậm nếu bundle lớn |
| Có kèm database (Postgres/MySQL) | 4GB | RAM thực tế cần để build và chạy ổn định khi có thêm DB cùng máy |
| Production, chạy kèm PostgreSQL | 8GB | Mức lý tưởng, tránh OOM cả lúc build và lúc chạy song song nhiều service |
Nếu đang cân đối thuê VPS cho việc này, mình đang làm ở InterData nên chia sẻ luôn: Xem Các Gói VPS Giá Tốt - bạn hoàn toàn có thể chọn nhà cung cấp khác phù hợp với nhu cầu và ngân sách của mình.
Auto-deploy khi push code
Nếu repo được kết nối qua GitHub App (không phải chỉ dán URL public), Coolify tự tạo webhook khi tạo Application. Vào tab cấu hình Application, bật Automatic Deployment, từ đó mỗi lần push lên branch đã chọn, Coolify tự trigger build và deploy lại, không cần vào dashboard bấm tay.
Kết bài
Coolify xử lý phần hạ tầng (Docker, Traefik, SSL) khá gọn, nhưng phần dễ mất thời gian nhất lại nằm ở hai chỗ: phân biệt đúng biến môi trường build-time/runtime, và chuẩn bị đủ RAM để build không bị OOM. Nắm được hai điểm này trước, quá trình deploy còn lại khá mượt.
All rights reserved