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

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ê.
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:
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.
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: testtạ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:
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:
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:
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ỳ).
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:
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:
Nếu health check fail, job báo đỏ ngay - bạn biết deploy lỗi trước khi user phàn nàn.
