Deploy NodeJS trên VPS: vì sao Dockerfile và Docker Compose “chuẩn” đáng giá từng phút cấu hình
Deploy NodeJS lên VPS tưởng đơn giản: git pull, npm install, pm2 restart. Chạy được. Nhưng vài tuần sau thường phát sinh: máy build lỗi vì khác version Node, app restart không tự lên, log khó đọc, rollback chậm, môi trường dev/prod lệch nhau, Nginx/DB/cache đụng port.
Bài này tập trung vào mẫu cấu hình thực dụng cho production trên VPS, đủ “sạch” để dùng lâu dài, đủ linh hoạt để mở rộng. Mục tiêu: build nhỏ, chạy ổn định, bảo mật cơ bản, dễ maintain.
Tư duy đúng trước khi viết Dockerfile
Nhiều dự án NodeJS gặp 3 lỗi phổ biến:
– Copy toàn bộ source quá sớm → cache build kém, build chậm – Chạy bằng root → rủi ro bảo mật – Dùng image nặng → tốn disk, tốn băng thông, startup chậm
Một Dockerfile tốt nên có:
– Base image rõ version – Tận dụng layer cache – Multi-stage build nếu có TypeScript/NestJS/Next build server – Chỉ mang dependency cần thiết sang production – User non-root – Healthcheck/log/env rõ ràng
Mẫu Dockerfile chuẩn cho NodeJS production
Giả sử app NodeJS dạng API, có bước build như TypeScript dist/.
# syntax=docker/dockerfile:1
FROM node:20-alpine AS base
WORKDIR /app
FROM base AS deps
COPY package*.json ./
RUN npm ci
FROM deps AS build
COPY . .
RUN npm run build
FROM node:20-alpine AS prod
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 3000
CMD ["node", "dist/main.js"]
Vì sao mẫu này tốt?
– node:20-alpine → nhẹ, đủ phổ biến cho prod
– COPY package*.json trước → đổi source không làm invalid cache cài package
– npm ci → cài đúng theo lockfile, ổn định hơn npm install
– Tách stage build/prod → image cuối nhỏ hơn
– --omit=dev → prod không mang devDependencies
– USER node → không chạy root
Khi nào cần chỉnh?
– App thuần JS, không build → bỏ stage build
– Dùng pnpm/yarn → thay lệnh cài package
– Dùng native module như sharp, bcrypt, prisma → có thể cần image Debian thay Alpine nếu gặp lỗi binary/libc
Ví dụ bản đơn giản cho app không build:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENV NODE_ENV=production
USER node
EXPOSE 3000
CMD ["node", "server.js"]
Đừng quên .dockerignore
Thiếu file này → image phình to, leak file nhạy cảm, build chậm.
node_modules
npm-debug.log
.git
.gitignore
Dockerfile
docker-compose.yml
.env
dist
coverage
.vscodeÝ nghĩa
– node_modules → tránh copy thư viện local vào container
– .git → giảm kích thước build context
– .env → tránh nhét secret vào image
– dist → nếu build trong Docker thì không cần copy từ máy host
Mẫu Docker Compose chuẩn để deploy trên VPS
Docker Compose giúp gom app + network + volume + restart policy trong một file dễ quản lý.
Ví dụ app NodeJS + PostgreSQL:
version: "3.9"
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: node_app
restart: unless-stopped
env_file:
- .env
ports:
- "3000:3000"
depends_on:
- postgres
networks:
- app_net
postgres:
image: postgres:16-alpine
container_name: postgres_db
restart: unless-stopped
environment:
POSTGRES_DB: appdb
POSTGRES_USER: appuser
POSTGRES_PASSWORD: strongpassword
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- app_net
volumes:
postgres_data:
networks:
app_net:
driver: bridge
Điểm quan trọng
– restart: unless-stopped → reboot VPS xong container tự lên
– env_file → tách config khỏi code
– depends_on → đảm bảo thứ tự start cơ bản
Lưu ý: nó không chờ DB ready hoàn toàn
– volumes → dữ liệu DB không mất khi recreate container
– networks → service giao tiếp nội bộ sạch hơn
Mẫu Compose tốt hơn cho production
Production thật thường không expose app trực tiếp. Thay vào đó:
– Nginx/Caddy → reverse proxy – Chỉ mở 80/443 – App chạy internal network – DB không public port
Ví dụ:
version: "3.9"
services:
app:
build: .
restart: unless-stopped
env_file:
- .env
expose:
- "3000"
networks:
- backend
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
networks:
- backend
networks:
backend:
driver: bridge
Vì sao expose tốt hơn ports?
– ports → publish ra ngoài VPS
– expose → chỉ mở cho container khác trong cùng network
App nội bộ → bề mặt tấn công thấp hơn.
Biến môi trường: chuẩn cách dùng để tránh rò rỉ
Nên giữ .env trên VPS, không commit Git.
Ví dụ:
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://appuser:strongpassword@postgres:5432/appdb
JWT_SECRET=change_meBest practice
– Không hardcode secret trong docker-compose.yml
– Không bake .env vào image
– Tách .env.production nếu có nhiều môi trường
– Rotate secret định kỳ nếu app quan trọng
Nếu cần bảo mật cao hơn → dùng Docker secrets, Vault, SSM. Nhưng với VPS nhỏ, .env + quyền file chuẩn đã đủ cho nhiều case.
Healthcheck, logging, tài nguyên: phần hay bị bỏ qua
Docker chạy được chưa đủ. Cần biết app có “sống khỏe” không.
Thêm healthcheck
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3
CMD wget -qO- http://localhost:3000/health || exit 1App nên có endpoint /health.
Giới hạn log
services:
app:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"Log không giới hạn → đầy disk VPS.
Giới hạn tài nguyên
services:
app:
mem_limit: 512m
cpus: 0.5App leak RAM → không kéo sập cả máy.
Quy trình deploy thực tế trên VPS
Một flow đơn giản, dễ áp dụng:
1. Chuẩn bị VPS
– Cài Docker
– Cài Docker Compose plugin
– Mở firewall cho 80, 443, 22
2. Upload code
git clone <repo>
cd <project>3. Tạo file môi trường
cp .env.example .env
nano .env4. Build và chạy
docker compose up -d --build5. Kiểm tra
docker compose ps
docker compose logs -f app6. Update phiên bản mới
git pull
docker compose up -d --buildFlow này tối ưu cho đa số app nhỏ-vừa, ít phụ thuộc CI/CD phức tạp.
Những lỗi phổ biến khi deploy NodeJS bằng Docker
1. Container chạy rồi thoát ngay
Nguyên nhân thường gặp:
– Sai CMD
– App crash khi boot
– Thiếu biến môi trường
– Build không tạo ra dist/
Kiểm tra:
docker compose logs -f app2. App không kết nối được DB
Thường do dùng localhost.
Trong Docker network:
– Sai → localhost:5432
– Đúng → postgres:5432
Tên host phải là tên service.
3. Permission lỗi khi ghi file
Nguyên nhân:
– Chạy bằng USER node
– Thư mục mount từ host không đúng quyền
Fix:
– Set quyền thư mục host phù hợp – Hoặc mount vào path app có thể ghi
4. Build rất chậm
Nguyên nhân:
– Thiếu .dockerignore
– COPY . . quá sớm
– Cache layer bị phá liên tục
Fix → giữ COPY package*.json trước, source sau.
Kết luận: “chuẩn” không phải phức tạp, mà là dễ vận hành
Một cấu hình Dockerfile và Docker Compose tốt cho NodeJS trên VPS nên đạt 4 mục tiêu:
– Nhẹ → image nhỏ, build nhanh – Ổn định → lockfile, restart policy, volume – An toàn hơn → non-root, app không expose trực tiếp – Dễ bảo trì → env tách riêng, logs rõ, update đơn giản
Nếu bạn chỉ nhớ 5 điểm cốt lõi, hãy nhớ:
– Dùng npm ci
– Có .dockerignore
– Multi-stage build khi cần
– Không chạy app bằng root
– Dùng Compose để quản lý app + DB + network
Từ nền này, bạn có thể đi tiếp sang Nginx reverse proxy, SSL tự động với Caddy/Traefik, CI/CD bằng GitHub Actions, zero-downtime deploy. Nhưng ngay cả trước khi tối ưu nâng cao, chỉ cần áp dụng đúng mẫu cơ bản trong bài viết này, bạn đã có một quy trình deploy NodeJS trên VPS sạch hơn, bền hơn, ít lỗi hơn rất nhiều.
Bình luận (0)
Chưa có bình luận. Hãy là người đầu tiên!