Giới thiệu tổng quan

Trong môi trường phát triển phần mềm hiện đại, việc tự động hoá quá trình xây dựng, kiểm thử và triển khai (CI/CD) không chỉ giảm thiểu lỗi con người mà còn tăng tốc độ đưa tính năng mới lên môi trường sản xuất. Bài viết này sẽ hướng dẫn chi tiết cách thiết lập một pipeline CI/CD hoàn chỉnh cho một ứng dụng Node.js, sử dụng Docker làm môi trường đóng gói và GitHub Actions làm công cụ tự động hoá, cuối cùng triển khai lên một VPS Linux thông qua SSH.

Yêu cầu chuẩn bị

  • Máy tính cá nhân có gitNode.js (phiên bản 18 trở lên) đã cài đặt.
  • Một tài khoản GitHub và một repository trống để chứa mã nguồn.
  • Một VPS Ubuntu 22.04 (hoặc Debian) với quyền sudo và cài đặt docker, docker-compose.
  • Khóa SSH công khai được thêm vào ~/.ssh/authorized_keys trên VPS để GitHub Actions có thể kết nối không cần mật khẩu.

Bước 1: Tổ chức dự án Node.js

Giả sử dự án đã có cấu trúc cơ bản như sau:

my-app/
├─ src/
│  └─ index.ts
├─ package.json
├─ tsconfig.json
└─ .gitignore

Chúng ta sẽ sử dụng TypeScript, vì vậy npm run build sẽ biên dịch mã nguồn sang thư mục dist. Đảm bảo package.json có các script sau:

{
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "ts-node-dev src/index.ts"
  }
}

Bước 2: Viết Dockerfile

Dockerfile sẽ tạo một image nhẹ dựa trên node:18-alpine, sao chép các file cần thiết, cài đặt phụ thuộc và chỉ định lệnh chạy ứng dụng.

# Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --production
EXPOSE 3000
CMD ["node", "dist/index.js"]

Ở đây chúng ta dùng multi‑stage build để giảm kích thước image cuối cùng, chỉ chứa các file đã biên dịch và các phụ thuộc production.

Bước 3: Cấu hình docker‑compose (tùy chọn)

Nếu dự án cần một cơ sở dữ liệu (ví dụ PostgreSQL) hoặc các service phụ trợ, chúng ta có thể dùng docker‑compose.yml để quản lý.

version: "3.8"
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
    depends_on:
      - db
  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: mydb
    volumes:
      - pgdata:/var/lib/postgresql/data
volumes:
  pgdata:

Trong pipeline CI/CD, chúng ta sẽ chỉ build image của service app và đẩy lên Docker registry riêng (hoặc Docker Hub).

Bước 4: Thiết lập GitHub Actions workflow

Tạo file .github/workflows/ci-cd.yml trong repository. Workflow sẽ bao gồm các job:

  1. Checkout code.
  2. Setup Node.js và cài đặt phụ thuộc.
  3. Chạy lint và test (nếu có).
  4. Build Docker image và đẩy lên Docker Hub.
  5. SSH vào VPS, pull image mới và khởi động lại container.

Dưới đây là ví dụ chi tiết:

name: CI/CD Pipeline

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run lint
        run: npm run lint --if-present

      - name: Run tests
        run: npm test --if-present

      - name: Build Docker image
        run: |
          docker build -t mydockerhubuser/my-app:${{ github.sha }} .

      - name: Log in to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Push Docker image
        run: |
          docker push mydockerhubuser/my-app:${{ github.sha }}

      - name: Deploy to VPS via SSH
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_SSH_KEY }}
          script: |
            docker pull mydockerhubuser/my-app:${{ github.sha }}
            docker stop my-app || true
            docker rm my-app || true
            docker run -d --name my-app -p 3000:3000 mydockerhubuser/my-app:${{ github.sha }}

Lưu ý:

  • Các biến môi trường nhạy cảm (DOCKERHUB_USERNAME, DOCKERHUB_TOKEN, VPS_HOST, VPS_USER, VPS_SSH_KEY) phải được lưu trong GitHub Secrets để không lộ ra công khai.
  • Job Deploy to VPS via SSH sử dụng action appleboy/ssh-action để thực thi các lệnh trên máy chủ từ xa.

Bước 5: Cấu hình máy chủ VPS

Trên VPS, chúng ta cần cài đặt Docker và Docker Compose (nếu dùng). Dưới đây là các lệnh cài đặt nhanh:

# Cập nhật hệ thống
sudo apt-get update && sudo apt-get upgrade -y

# Cài Docker
sudo apt-get install -y ca-certificates curl gnupg lsb-release
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

# Thêm người dùng hiện tại vào nhóm docker để chạy lệnh không sudo
sudo usermod -aG docker $USER
newgrp docker

Sau khi Docker đã sẵn sàng, chúng ta có thể kiểm tra bằng docker run hello-world. Khi pipeline GitHub Actions thực thi, nó sẽ tự động pull image mới và khởi động container.

Phần bảo mật và quản lý secret

Trong môi trường production, việc bảo mật thông tin nhạy cảm là yếu tố quan trọng. Dưới đây là một số khuyến nghị:

  • Sử dụng Docker secrets hoặc environment variables được truyền từ GitHub Actions để cung cấp DATABASE_URL, JWT_SECRET, v.v.
  • Không bao giờ commit file .env chứa secret vào repository.
  • Giới hạn IP truy cập SSH tới VPS bằng firewall (ufw) hoặc security group.
  • Đặt Dockerfile ở chế độ USER node (hoặc một user không root) để giảm rủi ro khi container bị tấn công.

Khắc phục các lỗi thường gặp

1. Lỗi docker: permission denied while trying to connect to the Docker daemon socket

Nguyên nhân thường là action SSH chạy dưới user không thuộc nhóm docker. Giải pháp: thêm user vào nhóm docker và khởi động lại SSH session, hoặc trong script sử dụng sudo docker ….

2. Lỗi manifest unknown: manifest unknown khi push image

Điều này xảy ra khi tag image không khớp với tên repository trên Docker Hub. Kiểm tra lại biến DOCKERHUB_USERNAME và tên repository.

3. Container không khởi động do cổng đã bị chiếm dụng

Sử dụng docker ps -a để kiểm tra container cũ, dừng và xóa chúng trước khi chạy container mới, hoặc thay đổi cổng mapping.

Best practices cho CI/CD với Docker

  • Luôn dùng multi‑stage build để giảm kích thước image.
  • Tag image bằng git SHAlatest để dễ rollback.
  • Sử dụng healthcheck trong Dockerfile để GitHub Actions có thể xác định container đã sẵn sàng chưa.
  • Giữ workflow ngắn gọn, tách riêng job testdeploy để tránh việc triển khai khi test thất bại.
  • Thiết lập rollback strategy bằng cách giữ lại image cũ và dùng docker tag để quay lại phiên bản ổn định.

Kết luận

Việc thiết lập một pipeline CI/CD tự động cho ứng dụng Node.js trên VPS không chỉ giúp giảm thiểu lỗi triển khai mà còn tạo ra quy trình phát triển nhất quán, nhanh chóng và an toàn. Khi đã nắm vững các bước từ viết Dockerfile, cấu hình GitHub Actions, đến bảo mật SSH và quản lý secret, bạn sẽ có thể mở rộng quy trình này cho các dự án phức tạp hơn, tích hợp database, cache, hoặc micro‑service.

Để nâng cao kiến thức và thực hành chi tiết hơn, Tham khảo khóa học "Deploy Web - Triển khai website lên môi trường Internet" tại đây.