Bạn đã có Server, đã cài Docker, Nginx. Nhưng mỗi lần sửa code lại phải SSH vào server, gõ git pull, build lại Docker Image thủ công?
Đã đến lúc tự động hóa mọi thứ để đạt trải nghiệm "Push là Lên" (như Vercel).
1. Khái niệm cốt lõi
CI/CD là gì?
- CI (Continuous Integration): Tích hợp liên tục. Khi bạn push code, hệ thống tự động chạy Test, Lint, Build để đảm bảo code không lỗi.
- CD (Continuous Deployment): Triển khai liên tục. Code sau khi build xong sẽ tự động được đẩy lên Server chạy.
Github Actions
Công cụ CI/CD tích hợp sẵn trong Github. Nó hoạt động dựa trên các Workflow (file cấu hình .yml).
- Runner: Máy chủ ảo của Github dùng để chạy lệnh build.
- Action: Các bước thực hiện (Checkout code, Login Docker, SSH...).
ArgoCD & GitOps (Level "Hero")
Trong các hệ thống lớn dùng Kubernetes, người ta không dùng Github Actions để SSH vào server (vì rủi ro bảo mật và khó quản lý nhiều server). Thay vào đó, họ dùng GitOps:
- Github Actions chỉ đóng vai trò Build Docker Image & đẩy lên Registry.
- ArgoCD (cài trong cụm Kubernetes) sẽ "canh chừng" (watch) Git Repository.
- Khi thấy file config thay đổi (ví dụ: image tag mới), ArgoCD sẽ tự động đồng bộ (Sync) về cụm K8s. -> Server tự kéo code về (Pull) thay vì bị đẩy code vào (Push).
2. Thực hành: Auto-Deploy Next.js lên VPS (GitHub Actions)
Vì chúng ta đang dùng 1 VPS đơn lẻ (chưa dùng Kubernetes), mô hình Push-based với GitHub Actions là hiệu quả và đơn giản nhất.
Mục tiêu:
- Code tại máy -> Push lên Main.
- GitHub Actions -> Build Docker Image -> Push lên Docker Hub.
- GitHub Actions -> SSH vào VPS -> Pull Image mới -> Restart Container.
Bước 1: Chuẩn bị Dockerfile cho Next.js
Tạo file Dockerfile ở thư mục gốc dự án Next.js:
# Base image nhẹ
FROM node:18-alpine AS base
# 1. Install dependencies only when needed
FROM base AS deps
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN npm ci
# 2. Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY . .
RUN npm run build
# 3. Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
COPY /app/public ./public
COPY /app/.next/standalone ./
COPY /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]Lưu ý: Trong next.config.js cần thêm output: 'standalone'.
Bước 2: Tạo Github Workflow
Tạo file .github/workflows/deploy.yml:
name: Deploy Next.js to VPS
on:
push:
branches: ["main"]
env:
DOCKER_IMAGE: huynguyen/my-blog # Thay bằng tên user/repo dockerhub của bạn
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- 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: ${{ env.DOCKER_IMAGE }}:latest
- name: Deploy to VPS via SSH
uses: appleboy/ssh-action@v0.1.6
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
docker pull ${{ env.DOCKER_IMAGE }}:latest
docker stop my-blog || true
docker rm my-blog || true
docker run -d \
--name my-blog \
--restart always \
-p 3000:3000 \
-e NODE_ENV=production \
${{ env.DOCKER_IMAGE }}:latestBước 3: Cấu hình Secret trên Github
Lưu ý quan trọng: Bạn phải vào phần cài đặt của Dự án (Repository), KHÔNG PHẢI cài đặt tài khoản cá nhân.
- Mở trang dự án của bạn trên GitHub (ví dụ:
https://github.com/huynv96/my-notes). - Nhìn lên thanh menu ngang trên cùng, chọn tab Settings (bên cạnh tab Insights).
- Ở menu bên trái, tìm mục Security -> chọn Secrets and variables -> chọn Actions.
- Nhấn nút màu xanh New repository secret.
Khi nhấn "New repository secret", bạn sẽ thấy 2 ô nhập:
- Name: Nhập tên biến (ví dụ
VPS_HOST). - Secret: Nhập giá trị thực tế (ví dụ
36.50.26.62).
Thêm lần lượt 5 bí mật sau:
1. DOCKERHUB_USERNAME
- Giá trị: Tên đăng nhập Docker Hub của bạn (ví dụ:
huynv96).
2. DOCKERHUB_TOKEN
Đây là mật khẩu dùng cho tool (an toàn hơn mật khẩu gốc).
- Truy cập Docker Hub Security Settings.
- Nhấn New Access Token.
- Description:
github-actions. - Access permissions: Chọn Read, Write, Delete.
- Copy chuỗi token vừa tạo (bắt đầu bằng
dckr_pat_...) và paste vào GitHub.
3. VPS_HOST
- Giá trị: Địa chỉ IP Public của VPS (ví dụ:
36.50.26.62).
4. VPS_USER
- Giá trị: Username bạn dùng để SSH vào VPS (ví dụ:
huynguyen).
5. VPS_SSH_KEY
Đây là bước quan trọng nhất và dễ nhầm lẫn nhất. GitHub cần một "chìa khóa" (Private Key) để mở cửa vào VPS của bạn.
Khuyên dùng: Nên tạo một chìa khóa riêng chuyên dụng cho GitHub Actions thay vì dùng chung chìa khóa cá nhân.
Cách tạo Key chuyên dụng (Thực hiện trên VPS):
- Đăng nhập vào VPS của bạn.
- Chạy lệnh tạo key mới (đặt tên là
github_action):bash:ssh-keygen -t ed25519 -C "github-actions" -f ~/.ssh/github_action -N "" - Cấp quyền cho chìa khóa này được phép "vào nhà" (thêm vào authorized_keys):
bash:
cat ~/.ssh/github_action.pub >> ~/.ssh/authorized_keys - Xem nội dung Private Key để copy:
bash:
cat ~/.ssh/github_action - Copy toàn bộ nội dung hiện ra (bao gồm cả dòng
-----BEGIN...và-----END...) dán vào GitHub Secret.
Lưu ý: Lệnh trên tạo key tên là
github_action. Nếu bạn kiểm tra bằngls ~/.ssh/sẽ thấy 2 file:github_action(Private - Cất đi) vàgithub_action.pub(Public - Đã gắn vào cửa).
Lưu ý: Lệnh trên tạo key tên là
github_action. Nếu bạn kiểm tra bằngls ~/.ssh/sẽ thấy 2 file:github_action(Private - Cất đi) vàgithub_action.pub(Public - Đã gắn vào cửa).
4. Cấu hình phía VPS (Production)
Sau khi thiết lập xong CI/CD, Container my-notes sẽ chạy ở cổng 3000 trên VPS. Để người dùng truy cập được bằng tên miền (ví dụ notes.english4it.site), bạn cần cấu hình Nginx làm cầu nối (Reverse Proxy).
A. Chuẩn bị Docker (Nếu Repo là Private)
Nếu Docker Image huynv96/my-notes bạn để chế độ Private, VPS cần đăng nhập mới kéo được ảnh về.
Thực hiện 1 lần duy nhất trên VPS:
docker login -u huynv96
# Nhập Password hoặc Access Token của bạnNếu Repository là Public (công khai) thì bỏ qua bước này.
B. Cấu hình Nginx Reverse Proxy
Bước 1: Tạo file cấu hình mới
sudo nano /etc/nginx/sites-available/my-notesBước 2: Dán nội dung cấu hình (Thay notes.english4it.site bằng tên miền bạn muốn):
server {
server_name notes.english4it.site;
location / {
proxy_pass http://127.0.0.1:3000; # Trỏ vào port 3000 của Next.js Container
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
# Thêm header để Next.js hiểu IP thật của user
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Bước 3: Kích hoạt site
# Tạo shortcut sang thư mục enabled
sudo ln -s /etc/nginx/sites-available/my-notes /etc/nginx/sites-enabled/
# Kiểm tra lỗi cú pháp
sudo nginx -t
# (Nếu OK) Reload Nginx
sudo systemctl reload nginxC. Cài SSL (HTTPS)
Cuối cùng, đừng quên khóa bảo mật bằng SSL miễn phí:
sudo certbot --nginx -d notes.english4it.siteD. Cách kiểm tra cấu hình thành công (Checklist)
Làm sao biết Nginx đã nhận cấu hình và hoạt động đúng? Hãy chạy lần lượt các lệnh sau trên VPS:
1. Kiểm tra cú pháp (Syntax Check)
Lệnh này quan trọng nhất, nó báo cho bạn biết file config có bị viết sai chính tả hay thiếu dấu ; không.
sudo nginx -t- ✅ Thành công: Báo
syntax is okvàtest is successful. - ❌ Thất bại: Sẽ hiện dòng lỗi cụ thể (ví dụ:
unknown directive at line 10).
2. Kiểm tra Nginx có đang chạy không
sudo systemctl status nginx- ✅ Thành công: Dòng
Active:hiện màu xanh láactive (running).
3. Kiểm tra Container App có đang chạy không
Nginx chỉ là người đưa thư, nếu App (Docker) chết thì Nginx sẽ báo lỗi 502 Bad Gateway.
docker ps | grep my-notes- ✅ Thành công: Thấy dòng chứa
my-notesvà port3000->3000.
4. Kiểm tra kết nối nội bộ (VPS tự gọi chính mình) Thử đóng vai Nginx gọi vào App xem App có trả lời không:
curl -I http://127.0.0.1:3000- ✅ Thành công: Thấy
HTTP/1.1 200 OK(hoặc 308/307). - ❌ Thất bại:
Connection refused(App chưa chạy hoặc sai port).
E. Xử lý lỗi 502 khi đăng nhập (Sign In)
Nếu giao diện web lên bình thường nhưng bấm Login bị lỗi 502 Bad Gateway:
Nguyên nhân: Thư viện bảo mật (Auth.js) chặn kết nối vì nó thấy yêu cầu đi qua Proxy (Nginx) mà chưa được "tin tưởng".
Cách sửa:
Bạn cần thêm biến môi trường AUTH_TRUST_HOST=true vào file deploy.yml.
- Mở file
.github/workflows/deploy.yml. - Tìm đoạn
docker run. - Thêm dòng
-e AUTH_TRUST_HOST=true \vào. - Cũng nên thêm
-e AUTH_URL=https://docs.english4it.site \(thay bằng domain thật của bạn).
Cách xem log lỗi chi tiết: Nếu vẫn lỗi, hãy xem server đang "kêu gào" gì bằng lệnh:
docker logs my-notes --tail 100 -f6. Xử lý biến môi trường (.env)
Dự án Next.js thường có file .env.local chứa key bảo mật. File này không bao giờ được đẩy lên Git, vì vậy server sẽ không biết các biến này nếu bạn không cài đặt.
Có 2 loại biến bạn cần thêm vào GitHub Secrets (cùng chỗ với bước 3):
A. Biến Build (NEXT_PUBLIC_...)
Next.js cần các biến này ngay khi Build Docker Image. Bạn cần thêm các secret sau vào GitHub:
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEYNEXT_PUBLIC_APP_URL(Ví dụ:https://notes.english4it.site)NEXT_PUBLIC_REPO_URL
B. Biến Runtime (Server Keys)
Đây là các key chỉ cần khi Code chạy trên server. Bạn cần thêm:
AUTH_SECRETSUPABASE_SERVICE_ROLE_KEY
Lưu ý: Tôi đã tự động cập nhật file
Dockerfilevàdeploy.ymlđể nhận các biến này. Bạn chỉ cần vào GitHub Settings và điền đúng giá trị (lấy từ file.envhoặc.env.localtrong máy bạn) là xong.
B. Biến Runtime (Server Keys)
Đây là các key chỉ cần khi Code chạy trên server. Bạn cần thêm:
AUTH_SECRETSUPABASE_SERVICE_ROLE_KEY
Lưu ý: Tôi đã tự động cập nhật file
Dockerfilevàdeploy.ymlđể nhận các biến này. Bạn chỉ cần vào GitHub Settings và điền đúng giá trị (lấy từ file.envhoặc.env.localtrong máy bạn) là xong.
7. Gỡ bỏ Vercel & Chuyển hẳng sang VPS
Nếu bạn đã thành công với Pipeline trên và thấy web chạy mượt trên VPS, bạn có thể "chia tay" Vercel để tiết kiệm chi phí hoặc đơn giản là muốn tự quản lý.
Bước 1: Tắt Git Integration trên Vercel
- Vào Dashboard Vercel -> Chọn dự án.
- Vào tab Settings -> Git.
- Tìm mục Connected Git Repository -> Bấm Disconnect.
- Tác dụng: Từ giờ khi bạn Push code lên GitHub, Vercel sẽ KHÔNG tự động build nữa (đỡ tốn tài nguyên build của Vercel).
Bước 2: Cấu hình lại DNS Domain
Hiện tại domain notes.english4it.site có thể đang trỏ về Vercel (CNAME hoặc A record). Bạn cần trỏ nó thẳng về VPS.
- Vào trang quản lý tên miền (Hostinger, Namecheap...).
- Xóa các bản ghi cũ liên quan đến Vercel (thường là CNAME
cname.vercel-dns.comhoặc IP76.76.21.21). - Thêm bản ghi A Record mới:
- Type: A
- Name:
@(hoặcnotesnếu là subdomain) - Value:
36.50.26.62(IP VPS của bạn) - TTL: Auto hoặc 300.
- Lưu lại và đợi 5-10 phút để mạng cập nhật.
Kết quả sau cùng:
- User truy cập domain -> Vào thẳng VPS của bạn (qua Nginx) -> Vào Docker Container Next.js.
- Vercel không còn tham gia vào quá trình này nữa.
8. Tổng kết
Với cấu hình trên, bạn đã có một quy trình "Semi-Professional":
- Tự động hóa Containerization (Docker).
- Tự động hóa Deployment (GitHub Actions).
- Zero downtime (gần như, chỉ mất vài giây restart container).
Khi dự án lớn lên hàng trăm Microservices, đó là lúc bạn cần nghiên cứu Kubernetes và ArgoCD để quản lý sự phức tạp đó. Còn với Blog/Web App đơn lẻ, cặp bài trùng Docker Compose + GitHub Actions là vô địch.