← ALL ARTICLES
11 Oct 2026/BLOG

Triển khai Next.js và NestJS lên VPS bằng Docker: Từ source code đến production - Basic

System Architecture, DevOps

Tags: Docker, Next.js, NestJS, DevOps, Linux

1. Kiến trúc triển khai

Một ứng dụng web fullstack thường có ít nhất ba thành phần: Đây là một kiến trúc cơ bản, phổ biến, dễ triển khai. Các bạn sinh viên có thể lấy làm mẫu để học deploy.

  • Frontend: Next.js, chịu trách nhiệm giao diện và rendering.
  • Backend: NestJS, cung cấp REST API hoặc các dịch vụ nghiệp vụ.
  • Database: PostgreSQL, lưu trữ dữ liệu ứng dụng.

Thay vì cài đặt Node.js và chạy từng ứng dụng trực tiếp trên VPS, chúng ta có thể đóng gói từng thành phần bằng Docker và quản lý chúng thông qua Docker Compose.

Kiến trúc mục tiêu:

                    Internet
                        |
                     HTTPS
                        |
                   Nginx / Proxy
                        |
              +---------+---------+
              |                   |
         Next.js :3000        NestJS :4000
              |                   |
              +-------------------+
                                  |
                              PostgreSQL

Trong môi trường production, database không nên được công khai trực tiếp ra Internet. Các dịch vụ nội bộ chỉ nên giao tiếp qua Docker network khi cần thiết.

Nginx hoặc một reverse proxy tương đương đảm nhiệm việc tiếp nhận kết nối HTTP/HTTPS và định tuyến đến các ứng dụng.

2. Tổ chức source code

Ví dụ cấu trúc repository:

my-app/
├── frontend/
│   ├── app/
│   ├── package.json
│   └── Dockerfile
├── backend/
│   ├── src/
│   ├── package.json
│   └── Dockerfile
├── compose.yaml
└── .dockerignore

Tách frontend và backend giúp mỗi ứng dụng có quy trình build, cấu hình và vòng đời triển khai riêng.

Trong một dự án lớn hơn, bạn có thể quản lý chúng bằng monorepo hoặc hai repository độc lập. Lựa chọn phụ thuộc vào cách tổ chức đội ngũ, quyền truy cập và quy trình phát hành.

3. Dockerfile cho NestJS

Ví dụ dưới đây giả định backend sử dụng npm, TypeScript và tạo ra thư mục dist sau khi build.

# backend/Dockerfile
FROM node:22-alpine AS build

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

FROM node:22-alpine AS production

WORKDIR /app
ENV NODE_ENV=production

COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force

COPY --from=build /app/dist ./dist

USER node

EXPOSE 4000

CMD ["node", "dist/main.js"]

Dockerfile sử dụng multi-stage build để tách môi trường build khỏi môi trường chạy.

Một số điểm cần chú ý:

  • npm ci cài dependencies theo lockfile.
  • Stage production không cần compiler TypeScript nếu ứng dụng đã được build thành JavaScript.
  • NODE_ENV=production giúp ứng dụng nhận biết môi trường chạy.
  • Chạy bằng user không phải root giúp giảm rủi ro khi container bị khai thác.

Nếu ứng dụng cần Prisma Client, file cấu hình, thư mục upload hoặc các tài nguyên khác, bạn phải bổ sung chúng vào image theo đúng cách ứng dụng sử dụng.

4. Dockerfile cho Next.js

Đối với Next.js, có thể sử dụng chế độ standalone để tạo image production gọn hơn.

Trong next.config.ts:

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "standalone",
};

export default nextConfig;

Dockerfile:

# frontend/Dockerfile
FROM node:22-alpine AS build

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

FROM node:22-alpine AS production

WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0

COPY --from=build --chown=node:node \
  /app/.next/standalone ./

COPY --from=build --chown=node:node \
  /app/.next/static ./.next/static

USER node

EXPOSE 3000

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

Cấu hình này giả định phiên bản Next.js và cấu trúc dự án hỗ trợ standalone output.

Nếu ứng dụng sử dụng thư mục public, hãy sao chép thêm thư mục đó khi cần. Với monorepo, vị trí standalone output có thể khác và cần điều chỉnh Dockerfile.

5. Quản lý các service bằng Docker Compose

Tạo file compose.yaml:

services:
  frontend:
    build:
      context: ./frontend
    restart: unless-stopped
    environment:
      PORT: "3000"
      HOSTNAME: "0.0.0.0"
    expose:
      - "3000"
    networks:
      - app-network

  backend:
    build:
      context: ./backend
    restart: unless-stopped
    environment:
      NODE_ENV: production
      PORT: "4000"
      DATABASE_URL: ${DATABASE_URL}
    expose:
      - "4000"
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

Đây là cấu hình minh họa cho hai ứng dụng, chưa phải toàn bộ stack production. Database và reverse proxy có thể được quản lý trong cùng Compose project hoặc tách riêng tùy nhu cầu vận hành.

Các điểm quan trọng:

  • restart: unless-stopped giúp container được khởi động lại trong những trường hợp phù hợp.
  • expose cho phép khai báo cổng phục vụ giao tiếp nội bộ, nhưng không tự công khai cổng ra host.
  • DATABASE_URL được lấy từ biến môi trường thay vì hardcode thông tin kết nối vào source code.
  • Docker network cho phép các service giao tiếp với nhau bằng tên service.

Lưu ý: biến môi trường trong Compose không tự động trở thành secret an toàn. Không commit file .env chứa mật khẩu thật lên Git.

6. Build và khởi chạy

Trên máy có source code:

docker compose build
docker compose up -d

Kiểm tra trạng thái:

docker compose ps
docker compose logs --tail=100 backend
docker compose logs --tail=100 frontend

Nếu ứng dụng không khởi động, hãy kiểm tra log trước khi thay đổi cấu hình một cách tùy tiện.

Một số nguyên nhân phổ biến:

  • Sai biến môi trường.
  • Backend không lắng nghe đúng port hoặc interface.
  • Database chưa sẵn sàng khi ứng dụng khởi động.
  • Thiếu file cần thiết trong Docker image.
  • Lỗi migration hoặc kết nối database.
  • Next.js không tìm thấy file runtime cần thiết.

Không nên mặc định rằng container đang chạy đồng nghĩa với ứng dụng hoạt động bình thường. Health check và kiểm tra endpoint thực tế sẽ cung cấp thêm bằng chứng về tình trạng hệ thống.

7. Những điều cần bổ sung trước khi đưa lên production

Một cấu hình Docker chạy được chưa đồng nghĩa với một quy trình production hoàn chỉnh.

Reverse proxy và HTTPS

Cấu hình Nginx hoặc reverse proxy tương đương để tiếp nhận HTTPS và chuyển tiếp request đến frontend hoặc backend.

Chỉ công khai các cổng cần thiết. Database và các service nội bộ không nên mở trực tiếp cho Internet.

Database và backup

Sử dụng volume phù hợp để lưu dữ liệu lâu dài. Thiết lập lịch backup, xác định thời gian lưu trữ và định kỳ thử phục hồi dữ liệu.

Backup chỉ thực sự đáng tin khi quá trình restore đã được kiểm chứng.

Logging và monitoring

Theo dõi log ứng dụng, mức sử dụng CPU, RAM, disk, dung lượng database và các lỗi HTTP quan trọng.

Đặt cảnh báo cho những chỉ số có thể ảnh hưởng trực tiếp đến người dùng.

Quy trình phát hành

Một quy trình CI/CD tốt nên có các bước:

  1. Kiểm tra code và chạy test.
  2. Build Docker image.
  3. Gắn tag image bằng commit SHA hoặc phiên bản phát hành.
  4. Đẩy image lên container registry.
  5. Máy chủ production tải image đã build và triển khai.
  6. Kiểm tra sức khỏe ứng dụng sau triển khai.
  7. Rollback về phiên bản trước nếu phát hành thất bại.

Với môi trường yêu cầu kiểm soát chặt chẽ, nên để CI build image và VPS chỉ pull image đã được tạo sẵn, thay vì build source code trực tiếp trên máy chủ production.

Kết luận

Docker giúp chuẩn hóa môi trường chạy ứng dụng, nhưng bản thân Docker không giải quyết toàn bộ bài toán vận hành.

Một hệ thống production cần sự kết hợp giữa image được build có kiểm soát, cấu hình an toàn, networking hợp lý, logging, monitoring, backup và quy trình phát hành có khả năng phục hồi.

Mục tiêu cuối cùng không chỉ là đưa ứng dụng lên VPS thành công. Đó là xây dựng một hệ thống có thể triển khai lặp lại, kiểm tra được và bảo trì lâu dài.

Một triển khai tốt là triển khai mà đội ngũ có thể hiểu, kiểm soát và phục hồi — không chỉ là triển khai chạy được một lần.