Đang tải...

Ếch Trendy

Cùng chú ếch nhỏ khám phá thế giới Web đầy biến động. Cập nhật trending nhanh như cách ếch đớp mồi! ⌨️🌿


0

GitHub Actions và Docker: CI/CD cho dự án web từ build đến deploy

Ếch Trendy
Ếch Trendy

4 tháng trước · 8 phút đọc

Tại sao cần CI/CD và tại sao lại là GitHub Actions + Docker?

Mình nhớ hồi đầu đi làm, mỗi lần deploy là một cuộc phiêu lưu. SSH vào server, git pull, chạy npm install, restart service - rồi ngồi cầu trời không có lỗi. Một lần deploy mất 20-30 phút, chưa kể hôm nào quên chạy migration thì coi như xong.

CI/CD (Continuous Integration / Continuous Deployment) giải quyết đúng vấn đề này. Mỗi khi push code lên GitHub, pipeline tự động chạy: test, build, deploy. Bạn chỉ cần merge PR và uống cà phê.

344607 Từ push code đến production - không cần SSH thủ công

GitHub Actions là lựa chọn tự nhiên nếu code đã ở GitHub - tích hợp sẵn, miễn phí cho repo public và có 2000 phút/tháng cho private. Docker đảm bảo môi trường chạy giống nhau từ máy dev đến server production - không còn cảnh "máy mình chạy được, server lại lỗi".

Kết hợp hai thứ này lại, bạn có một pipeline mạnh mẽ: code được đóng gói thành container chuẩn, test trong môi trường giống production, rồi deploy đồng nhất lên bất kỳ server nào.


Dockerfile cho dự án web: viết đúng từ đầu

Dockerfile tệ nhất mình từng thấy là copy toàn bộ node_modules vào image. Image nặng 2GB, build 10 phút. Tệ hơn là secrets bị commit vào layer.

Đây là Dockerfile chuẩn cho một dự án Node.js/React:

# Dùng multi-stage build để giảm kích thước image
FROM node:20-alpine AS builder

WORKDIR /app

# Copy package files trước - tận dụng Docker cache
COPY package*.json ./
RUN npm ci --only=production

# Copy source và build
COPY . .
RUN npm run build

# Stage production - chỉ lấy những gì cần
FROM node:20-alpine AS production

WORKDIR /app

# Chạy với user không phải root - bảo mật hơn
RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001

COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json

USER nextjs

EXPOSE 3000
CMD ["npm", "start"]

344608 Multi-stage build: stage 1 build nặng, stage 2 chỉ giữ production artifacts

Multi-stage build là kỹ thuật quan trọng nhất ở đây. Stage đầu dùng để compile/build, stage sau chỉ copy những file cần thiết. Kết quả: image production nhỏ hơn 50-80% (dev.to (Optimizing Docker Images: Reducing Size and Improving Security)) so với single-stage build thông thường.

Một điểm cần nhớ: đừng bao giờ copy file .env vào image. Secrets sẽ được inject lúc chạy container, không phải lúc build. Thêm .env vào .dockerignore ngay bây giờ.

# .dockerignore
.env
.env.*
node_modules
.git
*.log
.next/cache

Viết GitHub Actions workflow: từ trigger đến job

Workflow của GitHub Actions là file YAML trong .github/workflows/. Nghe có vẻ phức tạp nhưng cấu trúc khá dễ đọc khi bạn hiển rõ các khái niệm cơ bản.

# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

# Trigger: chạy khi push lên main hoặc mở PR
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  # Job 1: Chạy test
  test:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'  # Cache node_modules tự động
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run tests
        run: npm test
      
      - name: Run linting
        run: npm run lint

  # Job 2: Build và push Docker image
  build:
    needs: test  # Chỉ chạy khi test pass
    runs-on: ubuntu-latest
    # Chỉ build khi push lên main (không build trên PR)
    if: github.ref == 'refs/heads/main'
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Login Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_TOKEN }}
      
      - name: Build và push image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: yourusername/yourapp:latest

344609 needs: test đảm bảo build chỉ chạy khi test xanh - không deploy code lỗi

Mấy điểm quan trọng trong workflow này:

  • cache: 'npm' trong setup-node tự động cache node_modules. Lần đầu chạy hết 3-4 phút, những lần sau chỉ mất 30-40 giây nếu package.json không đổi.
  • needs: test tạo dependency giữa jobs. Build chỉ chạy khi test pass. Deploy chỉ chạy khi build xong.
  • if: github.ref == 'refs/heads/main' đảm bảo bạn không vô tình build Docker image cho mỗi PR nhỏ.

Cache trong CI/CD: tăng tốc pipeline từ 8 phút xuống 2 phút

Pipeline chạy lâu là kẻ thù của năng suất. Bạn push code, ngồi đợi 10 phút mới biết có lỗi không. Cache đúng cách là cách đơn giản nhất để rút ngắn thời gian này.

Có hai loại cache cần quan tâm:

1. Cache npm/yarn dependencies

Setup-node action đã làm điều này nếu bạn thêm cache: 'npm'. Nhưng nếu dùng Docker build bên trong Actions, bạn cần cache Docker layers riêng:

- name: Setup Docker Buildx
  uses: docker/setup-buildx-action@v3

- name: Build với cache
  uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: yourusername/yourapp:latest
    # Cache từ GitHub Actions cache
    cache-from: type=gha
    cache-to: type=gha,mode=max

344610 Cache Docker layers giúp bước COPY node_modules không chạy lại nếu package.json không đổi

2. Cache Docker layers - tận dụng layer ordering

Đây là lý do tại sao Dockerfile trên copy package*.json TRƯỚC khi copy source code. Docker cache hoạt động theo layer: nếu layer không đổi, nó dùng cache. Nếu package.json không thay đổi, lệnh npm ci sẽ được cache - dù source code thay đổi.

Thứ tự đúng trong Dockerfile:

1. COPY package*.json ./     ← ít thay đổi → cache lâu
2. RUN npm ci                ← được cache theo bước trên
3. COPY . .                  ← thay đổi thường xuyên
4. RUN npm run build         ← chạy lại khi source đổi

Thứ tự sai thì ngược lại: copy source trước, mỗi lần đổi 1 dòng code là npm ci chạy lại từ đầu.

Kết hợp cả hai cách, pipeline của mình giảm từ khoảng 8 phút xuống 1-2 phút cho các commit bình thường chỉ thay đổi source code.


Quản lý secrets và environment variables

Đây là phần mà mình thấy nhiều người làm sai nhất - hoặc hardcode secrets trong workflow, hoặc commit .env vào repo.

GitHub Actions có Secrets và Variables (Settings → Secrets and variables → Actions):

  • Secrets: cho sensitive data (API keys, passwords, tokens). Được mã hóa, không hiển thị trong logs.
  • Variables: cho config không nhạy cảm (NODE_ENV, APP_URL). Hiển thị trong logs.

Dùng trong workflow:

steps:
  - name: Deploy
    env:
      DATABASE_URL: ${{ secrets.DATABASE_URL }}
      APP_ENV: ${{ vars.APP_ENV }}
    run: ./deploy.sh

344611 Secrets được mask trong log - bạn sẽ thấy *** thay vì giá trị thật

Nếu dự án có nhiều môi trường (staging, production), dùng Environments trong GitHub:

jobs:
  deploy-staging:
    environment: staging  # Lấy secrets từ environment 'staging'
    steps:
      - run: echo ${{ secrets.DATABASE_URL }}  # URL staging
  
  deploy-production:
    environment: production  # Secrets khác hoàn toàn
    steps:
      - run: echo ${{ secrets.DATABASE_URL }}  # URL production

Environments còn cho phép bạn đặt required reviewers - deploy lên production phải được approve thủ công. Tính năng này cực kỳ hữu ích khi làm việc nhóm.

Một điều cần tránh: đừng echo secrets trong run step dù chỉ để debug. GitHub sẽ mask giá trị trong logs nhưng nếu bạn biến đổi secret (base64, substring...) trước khi echo thì có thể lộ.


Deploy thực tế: SSH vào server hoặc lên cloud

Phần này mới là lúc mọi thứ thật sự ý nghĩa. Build xong image rồi - đưa nó lên đâu?

Cách 1: Deploy lên VPS/server qua SSH

Phù hợp khi bạn có server riêng (DigitalOcean, Vultr, hoặc VPS bất kỳ).

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment: production
    
    steps:
      - name: Deploy lên server
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            # Pull image mới nhất
            docker pull yourusername/yourapp:latest
            
            # Dừng container cũ
            docker stop myapp || true
            docker rm myapp || true
            
            # Chạy container mới
            docker run -d \
              --name myapp \
              --restart unless-stopped \
              -p 3000:3000 \
              -e DATABASE_URL=$DATABASE_URL \
              yourusername/yourapp:latest

Cách 2: Deploy lên cloud platforms

Nếu dùng Railway, Render, hoặc Fly.io, thường đơn giản hơn nhiều:

      # Fly.io ví dụ
      - name: Deploy lên Fly.io
        uses: superfly/flyctl-actions/setup-flyctl@master
      
      - run: flyctl deploy --remote-only
        env:
          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

344612 SSH deploy phù hợp với VPS, cloud actions phù hợp khi muốn managed infrastructure

Mình thường dùng kết hợp: Docker Compose trên server để quản lý nhiều service cùng lúc (app, database, nginx). Script deploy chỉ cần docker compose pull && docker compose up -d.

Một tip quan trọng: sau khi deploy, thêm health check để verify container đang chạy:

      - name: Verify deployment
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            # Đợi container khởi động
            sleep 10
            # Kiểm tra HTTP response
            curl -f http://localhost:3000/health || exit 1
            echo "Deploy thành công!"

Nếu health check fail, job báo đỏ ngay - bạn biết deploy lỗi trước khi user phàn nàn.

Trang web dùng cookie để hoạt động và đo hiệu quả quảng cáo. Xem chính sách bảo mật