Giới thiệu

Trong môi trường phát triển phần mềm hiện đại, việc tự động hoá quy trình xây dựng, kiểm thử và triển khai (CI/CD) đã trở thành tiêu chuẩn bắt buộc để giảm thiểu rủi ro, tăng tốc độ đưa sản phẩm ra thị trường và duy trì chất lượng mã nguồn. Bài viết này sẽ tập trung vào cách thiết lập một pipeline CI/CD hoàn chỉnh cho Strapi CMS – một Headless CMS mã nguồn mở – sử dụng Docker làm môi trường container hoá và GitHub Actions làm công cụ tự động hoá. Nội dung sẽ đi sâu vào từng bước, cung cấp các ví dụ thực tế, so sánh ưu nhược điểm và đưa ra những best practices dành cho các team phát triển backend Node.js.

Tổng quan về CI/CD và Strapi

CI/CD là gì?

CI (Continuous Integration) là quá trình tích hợp mã nguồn mới vào nhánh chính một cách thường xuyên, kèm theo việc chạy các kiểm thử tự động để phát hiện lỗi sớm. CD (Continuous Delivery/Deployment) mở rộng CI bằng cách tự động hoá việc đóng gói và triển khai ứng dụng lên môi trường staging hoặc production. Khi CI/CD được thiết lập đúng, mỗi commit sẽ trải qua một chuỗi các bước: lint, test, build, và deploy, giúp giảm thiểu thời gian phản hồi và tăng độ tin cậy.

Strapi trong kiến trúc Headless CMS

Strapi là một framework Node.js cho phép tạo nhanh các API REST hoặc GraphQL, đồng thời cung cấp giao diện quản trị nội dung. Vì Strapi dựa trên Node.jsExpress, việc container hoá bằng Docker và chạy trong môi trường CI/CD là hoàn toàn khả thi. Ngoài ra, Strapi hỗ trợ plugin, middleware và các hook tùy chỉnh, cho phép mở rộng chức năng mà không làm phức tạp quá trình build.

Chuẩn bị môi trường phát triển

Cài đặt Docker và Docker Compose

Trước khi xây dựng pipeline, cần đảm bảo máy phát triển và máy CI có cài Docker (phiên bản >= 20.10) và Docker Compose (phiên bản >= 2.0). Docker cung cấp khả năng cô lập môi trường, giúp tránh các xung đột phụ thuộc giữa các dự án.

docker --version
docker-compose --version

Tạo dự án Strapi

Giả sử bạn chưa có dự án Strapi, có thể khởi tạo nhanh bằng lệnh sau:

npx create-strapi-app my-cms \
  --quickstart \
  --no-run

Sau khi dự án được tạo, cấu trúc thư mục sẽ bao gồm src, config, node_modules và file package.json. Để chuẩn bị cho Docker, chúng ta sẽ tạo một Dockerfile và một file docker-compose.yml cho môi trường development.

Dockerfile cho Strapi

Dockerfile sẽ dựa trên image node:18-alpine, cài đặt các phụ thuộc, sao chép mã nguồn và chạy lệnh npm run develop khi khởi động container.

FROM node:18-alpine

# Thiết lập thư mục làm việc
WORKDIR /app

# Sao chép package.json và lock file để tận dụng layer cache
COPY package*.json ./

# Cài đặt phụ thuộc
RUN npm ci --production

# Sao chép toàn bộ mã nguồn
COPY . .

# Thiết lập biến môi trường
ENV NODE_ENV=production

# Expose cổng mặc định của Strapi
EXPOSE 1337

# Lệnh khởi động
CMD ["npm", "run", "start"]

docker-compose.yml cho môi trường development

File này sẽ định nghĩa hai service: strapipostgres (hoặc sqlite nếu muốn đơn giản). Đối với môi trường production, chúng ta sẽ tách riêng file compose.

version: "3.8"
services:
  strapi:
    build: .
    ports:
      - "1337:1337"
    environment:
      - DATABASE_CLIENT=postgres
      - DATABASE_HOST=postgres
      - DATABASE_PORT=5432
      - DATABASE_NAME=strapi
      - DATABASE_USERNAME=strapi
      - DATABASE_PASSWORD=strapi
    depends_on:
      - postgres
    volumes:
      - .:/app
  postgres:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=strapi
      - POSTGRES_USER=strapi
      - POSTGRES_PASSWORD=strapi
    volumes:
      - pgdata:/var/lib/postgresql/data
volumes:
  pgdata:

Sau khi cấu hình xong, chạy docker-compose up -d để khởi động môi trường local. Việc này sẽ giúp chúng ta có một môi trường giống hệt như CI, giảm thiểu “works on my machine”.

Xây dựng pipeline GitHub Actions

Cấu trúc workflow

Trong GitHub, workflow được định nghĩa bằng file YAML nằm trong thư mục .github/workflows. Đối với Strapi, chúng ta sẽ tạo file ci-cd.yml với ba job chính:

  • lint: Kiểm tra định dạng code bằng ESLint.
  • test: Chạy các unit test và integration test.
  • build-and-deploy: Build Docker image, push lên Docker Hub và triển khai lên VPS.

Job lint

Job này sử dụng Node.js 18, cài đặt các phụ thuộc và chạy lệnh npm run lint. Nếu lint thất bại, pipeline sẽ dừng lại.

Job test

Đối với Strapi, chúng ta có thể viết các test bằng Jest kết hợp supertest để kiểm tra các endpoint API. Dưới đây là một ví dụ test đơn giản cho route /api/articles:

const request = require('supertest');
const strapi = require('../../src'); // Giả sử Strapi được khởi tạo ở đây

describe('GET /api/articles', () => {
  it('should return a list of articles', async () => {
    const res = await request(strapi).get('/api/articles');
    expect(res.statusCode).toBe(200);
    expect(Array.isArray(res.body.data)).toBe(true);
  });
});

Đảm bảo file jest.config.js đã cấu hình môi trường test phù hợp, ví dụ:

module.exports = {
  testEnvironment: 'node',
  roots: ['/tests'],
  coverageDirectory: '/coverage',
};

Job build-and-deploy

Job này thực hiện các bước:

  1. Login vào Docker Hub bằng secret DOCKERHUB_TOKEN.
  2. Build Docker image với tag dựa trên commit SHA.
  3. Push image lên Docker Hub.
  4. SSH vào VPS và thực thi docker-compose pull && docker-compose up -d để cập nhật container.

Dưới đây là nội dung file ci-cd.yml (đã được rút gọn để tập trung vào các bước quan trọng):

name: CI/CD for Strapi

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

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm ci
      - run: npm run lint

  test:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v3
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm ci
      - run: npm test

  build-and-deploy:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v3
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
      - name: Login to DockerHub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Build and push Docker image
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: ${{ secrets.DOCKERHUB_USERNAME }}/strapi:${{ github.sha }}
      - name: Deploy to VPS
        env:
          SSH_PRIVATE_KEY: ${{ secrets.VPS_SSH_KEY }}
          VPS_HOST: ${{ secrets.VPS_HOST }}
          VPS_USER: ${{ secrets.VPS_USER }}
        run: |
          echo "$SSH_PRIVATE_KEY" > key.pem
          chmod 600 key.pem
          ssh -o StrictHostKeyChecking=no -i key.pem $VPS_USER@$VPS_HOST <<'EOF'
            cd /opt/strapi
            docker-compose pull
            docker-compose up -d
          EOF

Trong ví dụ trên, chúng ta sử dụng các secret để bảo mật thông tin đăng nhập Docker Hub và SSH vào VPS. Các secret này cần được cấu hình trong phần Settings → Secrets của repository.

Triển khai lên môi trường Production

Docker Swarm hoặc Docker Compose trên VPS

Đối với các dự án nhỏ‑trung bình, Docker Compose vẫn là lựa chọn đơn giản và hiệu quả. Tuy nhiên, nếu dự án cần mở rộng quy mô, Docker Swarm hoặc Kubernetes sẽ cung cấp khả năng cân bằng tải và tự động phục hồi. Trong bài viết này, chúng ta sẽ tiếp tục sử dụng Docker Compose trên một VPS Ubuntu 22.04.

Cấu hình biến môi trường

Biến môi trường quan trọng bao gồm DATABASE_URL, HOST, PORT, ADMIN_JWT_SECRET, và JWT_SECRET. Để tránh rò rỉ thông tin, chúng ta sẽ lưu các biến này trong file .env.production và mount vào container.

# .env.production
HOST=0.0.0.0
PORT=1337
DATABASE_CLIENT=postgres
DATABASE_HOST=postgres
DATABASE_PORT=5432
DATABASE_NAME=strapi_prod
DATABASE_USERNAME=strapi_prod
DATABASE_PASSWORD=StrongP@ssw0rd!
ADMIN_JWT_SECRET=SuperSecretAdminKey
JWT_SECRET=SuperSecretJwtKey

File docker-compose.yml cho production sẽ tham chiếu tới file env này:

version: "3.8"
services:
  strapi:
    image: yourdockerhubusername/strapi:${{ github.sha }}
    env_file:
      - .env.production
    ports:
      - "1337:1337"
    depends_on:
      - postgres
    restart: always
  postgres:
    image: postgres:15-alpine
    env_file:
      - .env.production
    volumes:
      - pgdata_prod:/var/lib/postgresql/data
    restart: always
volumes:
  pgdata_prod:

Quy trình cập nhật không downtime

Để tránh downtime khi cập nhật phiên bản mới, chúng ta có thể áp dụng chiến lược blue‑green deployment bằng cách chạy hai stack Docker Compose (blue và green) trên cùng một VPS, chuyển đổi Nginx proxy sau khi container mới đã sẵn sàng. Đây là một kỹ thuật nâng cao, nhưng rất hữu ích cho các hệ thống cần tính sẵn sàng 99.9%.

Best practices và lưu ý bảo mật

  • Giữ Dockerfile tối thiểu: Sử dụng multi‑stage build để giảm kích thước image, loại bỏ các dev dependencies.
  • Sử dụng .dockerignore để tránh copy các file không cần thiết (node_modules, .git, tests).
  • Quản lý secret: Không bao giờ hard‑code mật khẩu trong Dockerfile hoặc compose. Dùng secret manager của cloud provider hoặc GitHub Secrets.
  • Kiểm thử bảo mật: Thêm bước scan image bằng Trivy hoặc Anchore trong workflow để phát hiện lỗ hổng.
  • Giám sát logs: Đẩy logs của Strapi và PostgreSQL tới một hệ thống tập trung (ELK, Loki) để dễ dàng debug.
  • Backup database: Thiết lập cron job sao lưu PostgreSQL và lưu trữ ở S3 hoặc Google Cloud Storage.

Kết luận

Việc thiết lập CI/CD cho Strapi CMS không chỉ giúp giảm thiểu lỗi phát sinh trong quá trình triển khai mà còn tạo ra một quy trình chuẩn hoá, dễ mở rộng cho các dự án Headless CMS trong tương lai. Khi áp dụng các best practices như multi‑stage Docker build, secret management và blue‑green deployment, bạn sẽ có một hệ thống ổn định, bảo mật và có khả năng đáp ứng nhanh các yêu cầu thay đổi của khách hàng. Để nắm bắt toàn bộ quy trình từ khởi tạo dự án, viết API, tới triển khai production một cách chi tiết, bạn có thể Tham khảo khóa học "Xây dựng Back-End Nodejs bằng Strapi CMS" tại đây.