← ALL CASE STUDIES
CI/CD/27 Sept 2026

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

DevOps

ARCHITECTURE INDEX / 01STATUS: VERIFIED

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.