Dựng CI/CD phát triển liên tục cho doanh nghiệp
DevOps

Building a Production-Ready CI/CD Pipeline with GitHub Actions and Docker
Role: DevOps · Tech Lead · Backend/Fullstack Engineer Focus: CI/CD · Docker · Linux · GitHub Actions · Deployment Architecture Environment: Ubuntu VPS · Docker · Docker Compose · GitHub
The Problem
Khi một ứng dụng bắt đầu được sử dụng trong production, việc deploy bằng tay nhanh chóng trở thành một vấn đề.
Một quy trình ban đầu có thể trông như:
Developer
↓
git push
↓
SSH vào server
↓
git pull
↓
docker compose build
↓
docker compose up
Nó có thể hoạt động với một project nhỏ.
Nhưng khi số lượng service tăng lên, team có nhiều developer hơn và production trở nên quan trọng, quy trình này bắt đầu xuất hiện nhiều rủi ro:
- server phải chứa source code;
- server phải build application;
- build có thể tiêu tốn CPU/RAM;
- deployment phụ thuộc vào thao tác thủ công;
- khó rollback;
- khó kiểm soát version đang chạy;
- khó audit ai deploy và deploy version nào.
Tôi muốn thay đổi cách tiếp cận.
Thay vì để production server build software, tôi muốn production server chỉ làm một nhiệm vụ:
Run software.
The Architecture
Kiến trúc tôi hướng tới:
Developer
│
│ git push
▼
GitHub
│
▼
GitHub Actions
│
├── Test
├── Build
├── Docker Image
└── Push
│
▼
Container Registry
│
│ pull image
▼
Production VPS
│
▼
Docker Compose
│
┌─────┴─────┐
▼ ▼
Frontend Backend
Điểm quan trọng là:
Production server không cần source code để build application.
Server chỉ pull image đã được build và chạy image đó.
Why Docker Images?
Một trong những vấn đề của deployment truyền thống là môi trường build và môi trường production có thể khác nhau.
Ví dụ:
Developer machine
Node.js 22
npm version A
Production
Node.js 20
npm version B
Một application chạy được ở local chưa đảm bảo nó chạy giống hệt trên production.
Docker giúp tôi đóng gói application cùng runtime environment.
Ví dụ:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD ["node", "dist/main.js"]
Sau khi build:
Source Code
↓
Docker Build
↓
Immutable Image
Image trở thành artifact của deployment.
Build Once, Run Anywhere
Tôi thích một nguyên tắc đơn giản:
Build once, run the same artifact everywhere.
Ví dụ:
Commit abc123
↓
Docker Image
↓
Registry
↓
Staging
↓
Production
Nếu production đang chạy:
my-app:abc123
thì tôi biết chính xác production đang chạy artifact nào.
Thay vì:
"Server build lại source mới nhất."
tôi muốn:
"Server chạy đúng artifact đã được CI tạo ra."
Đây là khác biệt rất quan trọng trong việc kiểm soát deployment.
CI Pipeline
Pipeline bắt đầu ngay khi code được push.
Push
↓
Checkout
↓
Install dependencies
↓
Lint
↓
Test
↓
Build
↓
Docker Build
↓
Docker Push
Nếu test fail:
Test
↓
FAIL
X
Production
Deployment không được tiếp tục.
Điều này giúp CI trở thành một quality gate thay vì chỉ là một script build.
CD Pipeline
Sau khi image được push lên registry:
Docker Registry
│
│ pull
▼
Production VPS
│
▼
docker compose pull
│
▼
docker compose up -d
Server không cần:
git pull
npm install
npm build
docker build
Nó chỉ cần pull artifact đã được build.
Separating Build and Runtime
Một quyết định khác tôi ưu tiên là tách build environment và runtime environment.
CI
│
┌──────┴──────┐
│ │
Build Test
│ │
└──────┬──────┘
↓
Docker Image
↓
Registry
↓
Production
Production server không cần biết:
- source code repository;
- npm dependencies;
- build toolchain;
- compiler;
- CI configuration.
Nó chỉ cần:
Docker
Docker Compose
Network
Storage
Secrets
Điều này giúp production server trở nên đơn giản hơn.
Docker Compose as the Runtime Definition
Thay vì chạy container bằng những command rời rạc:
docker run ...
docker run ...
docker network ...
tôi sử dụng Docker Compose để mô tả application stack.
Ví dụ:
services:
frontend:
image: registry.example.com/app/frontend:latest
backend:
image: registry.example.com/app/backend:latest
redis:
image: redis:alpine
postgres:
image: postgres:16
Compose trở thành một phần của deployment architecture.
Nó mô tả:
Application
├── Frontend
├── Backend
├── Database
└── Redis
Từ đó production environment trở nên reproducible hơn.
Secrets Are Not Source Code
Một bài học quan trọng khác là:
Configuration và secrets không nên được hard-code vào Docker image.
Ví dụ:
Docker Image
│
├── Application
├── Dependencies
└── Build artifacts
Runtime
│
├── DATABASE_URL
├── REDIS_URL
├── JWT_SECRET
└── API_KEYS
Application image có thể được dùng ở nhiều environment.
Configuration được inject khi container chạy.
Điều này giúp tách:
Code
≠
Environment
≠
Secret
Deployment Without Downtime Is a Different Problem
Một deployment đơn giản:
docker compose down
docker compose up -d
có thể tạo downtime.
Khi production requirements cao hơn, deployment strategy phải được suy nghĩ khác.
Ví dụ:
Current Version
↓
New Version
↓
Health Check
↓
Traffic Switch
Tùy quy mô hệ thống, có thể phát triển thành:
- rolling deployment;
- blue-green deployment;
- reverse proxy switching;
- container health checks;
- load balancing.
Không phải project nào cũng cần tất cả những thứ này.
Một nguyên tắc tôi theo đuổi là:
Architecture should match the actual operational requirement.
Không cần Kubernetes chỉ vì Kubernetes nghe "enterprise".
Health Checks
Container chạy không đồng nghĩa application healthy.
Một service có thể:
Container: Running
Application: Broken
Do đó health check có thể được sử dụng:
Container
↓
Health Endpoint
↓
Database
↓
Redis
↓
Application Ready
Ví dụ:
GET /health
Response:
{
"status": "ok"
}
Health check là một phần nhỏ nhưng rất quan trọng của production architecture.
Rollback
Một lợi ích lớn của image-based deployment là rollback trở nên rõ ràng hơn.
Ví dụ:
Production
│
├── v1.8
└── v1.9 ← current
Nếu v1.9 có vấn đề:
v1.9
↓
Problem
↓
Rollback
↓
v1.8
Không cần:
git checkout
npm install
npm build
trên production server.
Chỉ cần chạy lại artifact đã được kiểm chứng trước đó.
Observability
CI/CD không kết thúc ở deployment.
Sau deployment, câu hỏi quan trọng hơn là:
Application có thực sự hoạt động không?
Tôi bắt đầu quan tâm đến:
Deployment
↓
Container Status
↓
Health Check
↓
Application Logs
↓
Resource Usage
↓
User Traffic
Một deployment pipeline tốt không chỉ biết:
"Deploy thành công."
Nó phải tiến gần hơn tới:
"Version mới đã được deploy và hệ thống đang healthy."
The Human Side of DevOps
Điều tôi nhận ra sau khi xây dựng những workflow kiểu này là DevOps không chỉ là:
Docker
Linux
GitHub Actions
Mục tiêu cuối cùng là giảm phụ thuộc vào con người.
Nếu deployment yêu cầu một engineer nhớ:
command A
→ command B
→ command C
→ sửa file D
→ restart service E
thì đó vẫn là một quy trình dễ sai.
Tôi muốn biến nó thành:
git push
↓
CI
↓
Artifact
↓
Deployment
↓
Health Check
Con người tập trung vào engineering decision.
Máy móc thực hiện những bước lặp lại.
What I Learned
1. Server should not necessarily build the application
Build và runtime có những yêu cầu khác nhau.
Tách chúng giúp production environment đơn giản và predictable hơn.
2. Deployment is an engineering system
Deployment không chỉ là:
"Đưa code lên server."
Nó là một hệ thống gồm:
Source
↓
Build
↓
Test
↓
Artifact
↓
Registry
↓
Deployment
↓
Health
↓
Rollback
3. Reproducibility matters
Nếu tôi không thể trả lời:
"Production đang chạy artifact nào?"
thì tôi chưa kiểm soát production đủ tốt.
4. Automation reduces operational risk
Automation không thay thế engineer.
Nó giải phóng engineer khỏi những công việc lặp lại và dễ sai.
Final Architecture
Sau cùng, cách tôi muốn nhìn một production deployment là:
Developer
│
│ git push
▼
GitHub
│
▼
GitHub Actions
│
┌──────────┴──────────┐
│ │
Test Build
│ │
└──────────┬──────────┘
▼
Docker Image
│
▼
Container Registry
│
│ pull
▼
Production VPS
│
Docker Compose
│
┌──────────┼──────────┐
▼ ▼ ▼
Frontend Backend Workers
│ │ │
└──────────┼──────────┘
▼
Health Checks
│
▼
Production
Điều tôi muốn đạt được không phải là một pipeline "xịn".
Tôi muốn một pipeline mà khi developer push code, hệ thống có thể biến commit đó thành một artifact có version rõ ràng, kiểm thử nó, đưa nó lên production một cách có kiểm soát và cho phép rollback khi cần.
Good DevOps is not about having more tools.
It is about making the path from code to production predictable.
Và đó cũng là cách tôi nhìn về infrastructure:
Không chỉ làm cho hệ thống chạy. Mà làm cho hệ thống có thể được vận hành, kiểm soát và thay đổi một cách an toàn.