Đ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

DevOps Docker CI/CD GitHub Actions cho Web Developer Việt 2026

Ếch Trendy
Ếch Trendy

6 tháng trước · 9 phút đọc

Tại sao dev Việt cần biết Docker và CI/CD ngay bây giờ?

Bạn có bao giờ deploy xong local chạy ngon, lên server lại crash? Hoặc cả team 5 người mà môi trường mỗi người một kiểu, merge code là cầu nguyện?

Đó là vấn đề mà Docker và CI/CD sinh ra để giải quyết. Không phải chỉ cho big tech - mà cho bất kỳ team nào muốn ship code nhanh và ít bug hơn.

343123 Vòng lặp deploy thủ công tốn thời gian hơn bạn nghĩ

Theo khảo sát {{fact:fact_0}}, các team áp dụng CI/CD giảm thời gian deploy trung bình xuống còn dưới 10 phút so với quy trình thủ công. Với GitHub Actions hiện tại cho {{fact:fact_1}} free minutes mỗi tháng cho public repo, barrier to entry gần như bằng 0.

Bài này mình sẽ đi từ Dockerfile cho Next.js 16, qua pipeline GitHub Actions, đến cách quản lý secrets trong team - không lý thuyết dài dòng, thẳng vào code chạy được.


Dockerfile cho Next.js 16: multi-stage build thực tế

Build Docker image "naive" cho Next.js có thể lên tới 1.2GB. Với multi-stage build, con số đó rút xuống còn khoảng 120MB. Không phải magic - chỉ là bỏ đi mọi thứ không cần thiết lúc runtime.

Logic đơn giản: stage builder cài đủ deps + build ra .next/standalone, stage runner chỉ copy output đó vào và chạy. Node modules, source code, cache - tất cả bị bỏ lại.

# Stage 1: Build
FROM node:22-alpine AS builder
WORKDIR /app

# Copy package files trước để tận dụng layer cache
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile

COPY . .
RUN pnpm build  # output ra .next/standalone (Next.js 13+)

# Stage 2: Production runner
FROM node:22-alpine AS runner
WORKDIR /app

# Tạo user riêng, không chạy root
RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 nextjs

# Chỉ copy những gì cần thiết
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]

343124 120MB thay vì 1.2GB - 10x nhỏ hơn nhờ multi-stage

Một điểm hay ho: copy package.jsonpnpm-lock.yaml trước khi copy source code. Khi bạn chỉ sửa code mà không đổi deps, Docker sẽ dùng lại layer cache pnpm install - tiết kiệm vài phút mỗi build.

Trong next.config.js, cần bật standalone mode:

// next.config.js
module.exports = {
  output: 'standalone',  // Bắt buộc để multi-stage hoạt động
};

Trade-off cần biết: Standalone mode copy cả node_modules cần thiết vào output, nên lần đầu build vẫn lâu. Dùng BuildKit cache mount để tăng tốc:

# Mount cache để pnpm không re-download mỗi lần
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
    corepack enable && pnpm install --frozen-lockfile

Thêm .dockerignore để tránh copy rác vào build context:

node_modules
.next
.git
*.log
.env.local

Không có .dockerignore thì COPY . . sẽ copy cả node_modules vào context - chậm và thừa.


GitHub Actions pipeline: deploy Vercel và AWS ECS

Có Dockerfile rồi, bước tiếp là tự động hóa. Mình sẽ show 2 pipeline phổ biến nhất: Vercel cho frontend nhanh, AWS ECS cho production cần control hơn.

Deploy Vercel với GitHub Actions

Vercel có GitHub App integration tự động tạo preview URL khi bạn push PR. Nhưng nếu muốn control flow deploy (ví dụ: chỉ deploy sau khi test pass), dùng Actions manually:

# .github/workflows/vercel-deploy.yml
name: Deploy to Vercel
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read
  id-token: write  # Cần cho OIDC

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'pnpm'

      - run: corepack enable && pnpm install --frozen-lockfile
      - run: pnpm test  # Chạy test trước khi deploy
      - run: pnpm build

      - uses: vercel/action@v1
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          # Deploy production chỉ khi push lên main
          vercel-args: ${{ github.ref == 'refs/heads/main' && '--prod' || '' }}

343125 PR tạo preview URL tự động, merge lên main thì production deploy

Deploy AWS ECS với Docker

Với AWS, flow sẽ là: build Docker image → push lên ECR → update ECS task definition → deploy. Điểm quan trọng là dùng OIDC thay vì lưu AWS access key trong secrets - an toàn hơn nhiều:

# .github/workflows/aws-deploy.yml
name: Deploy AWS ECS
on:
  push:
    branches: [main]

permissions:
  contents: read
  id-token: write  # Bắt buộc cho OIDC với AWS

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Setup Buildx để dùng cache
      - uses: docker/setup-buildx-action@v3

      # Auth AWS qua OIDC - không cần lưu access key!
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: ap-southeast-1  # Singapore gần VN
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}

      # Login ECR
      - uses: docker/login-action@v3
        with:
          registry: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-southeast-1.amazonaws.com

      # Build và push với cache
      - uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-southeast-1.amazonaws.com/web:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      # Deploy lên ECS
      - uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: task-definition.json
          service: web-service
          cluster: web-cluster
          wait-for-service-stability: true

Dùng github.sha làm tag thay vì latest - khi cần rollback biết chính xác version nào đang chạy. latest là anti-pattern trong production.

Tối ưu build time với cache-from: type=gha: GitHub Actions cache Docker layers giữa các run. Build đầu tiên vẫn lâu, từ lần 2 trở đi nhanh hơn đáng kể vì chỉ rebuild layer thay đổi.


Monorepo với Turborepo: cấu trúc cho team Việt

Monorepo không có nghĩa là gộp mọi thứ vào một repo lộn xộn. Làm đúng thì bạn có shared components, shared config, và build chỉ compile những gì thay đổi - không build lại toàn bộ.

Turborepo là lựa chọn thực tế nhất cho team Việt: open source, tích hợp tốt với pnpm workspaces, và không cần trả tiền như Nx enterprise.

Cấu trúc thư mục

monorepo/
├── apps/
│   ├── web/          # Next.js - frontend chính
│   └── api/          # Express/Fastify - backend
├── packages/
│   ├── ui/           # Shared components (Button, Modal...)
│   └── config/       # ESLint, TSConfig dùng chung
├── turbo.json
├── pnpm-workspace.yaml
└── package.json

pnpm-workspace.yaml khai báo workspace:

packages:
  - 'apps/*'
  - 'packages/*'

turbo.json định nghĩa task dependencies:

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**"]
    },
    "lint": {},
    "type-check": {}
  }
}

dependsOn: ["^build"] có nghĩa: trước khi build apps/web, phải build xong packages/uipackages/config. Turborepo tự xử lý dependency graph này.

343126 Turborepo chỉ rebuild package nào thay đổi - không phải toàn bộ monorepo

GitHub Actions với monorepo

Trick hay: chỉ trigger deploy khi có thay đổi trong app tương ứng, dùng paths filter:

# Chỉ deploy web khi apps/web hoặc packages/* thay đổi
on:
  push:
    branches: [main]
    paths:
      - 'apps/web/**'
      - 'packages/**'
      - 'turbo.json'
      - 'pnpm-lock.yaml'

Với monorepo, build trong Actions cũng cần cache Turborepo:

- name: Cache Turborepo
  uses: actions/cache@v4
  with:
    path: .turbo
    key: ${{ runner.os }}-turbo-${{ github.sha }}
    restore-keys: |
      ${{ runner.os }}-turbo-

- run: pnpm turbo build --filter=web...  # Chỉ build web và deps của nó

--filter=web... (3 chấm) build web và tất cả packages mà web depend on. Không build api nếu không cần.

Mình từng thử Nx cho một dự án, overkill với team nhỏ. Turborepo đủ dùng cho 90% team Việt đang build startup.


Secrets management: đừng để lộ credentials của team

"Works on my machine" là joke quen thuộc. Nhưng "leaked credentials on GitHub" thì không ai cười được.

Mình thấy khá nhiều team Việt vẫn commit .env lên repo, hoặc share access key qua Zalo. Đó là rủi ro không đáng. Có 3 layer cần biết:

Layer 1: GitHub Secrets - đủ dùng cho hầu hết team

Vào Settings → Secrets and variables → Actions để thêm secrets. Trong workflow dùng ${{ secrets.TEN_SECRET }}.

Với AWS, đừng lưu access key. Dùng OIDC thay thế - AWS cấp token tạm thời cho mỗi workflow run:

# Trên AWS: tạo IAM Role với trust policy cho GitHub
# Trong workflow:
- uses: aws-actions/configure-aws-credentials@v4
  with:
    aws-region: ap-southeast-1
    role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
    # Không cần AWS_ACCESS_KEY_ID hay AWS_SECRET_ACCESS_KEY

Token tự hết hạn sau mỗi run. Không có long-lived credentials nào để leak.

Layer 2: Doppler - khi team cần centralized secrets

Doppler cho phép sync secrets từ một nơi ra nhiều môi trường (dev/staging/prod) và nhiều services. Tích hợp với Docker:

# Thay vì truyền -e ENV_VAR=value
doppler run -- docker compose up
# Trong GitHub Actions:
- uses: dopplerhq/[email protected]
  with:
    doppler-token: ${{ secrets.DOPPLER_TOKEN }}
    inject-env-vars: true

343127 3 lớp secrets management - chọn theo quy mô team

Layer 3: AWS Secrets Manager - cho production nghiêm túc

App runtime cần credentials database, API keys? Lưu trong AWS Secrets Manager và fetch trong code:

// Fetch secret lúc app khởi động, không hard-code
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';

const client = new SecretsManagerClient({ region: 'ap-southeast-1' });
const secret = await client.send(new GetSecretValueCommand({
  SecretId: 'prod/myapp/database',
}));
const { DB_PASSWORD } = JSON.parse(secret.SecretString);

Rotate secrets định kỳ: GitHub Secrets thì rotate theo quý. Nếu dùng Doppler hoặc AWS Secrets Manager thì có thể setup auto-rotation.

Quick checklist trước khi push code:

  • .env.env.local đã có trong .gitignore?
  • Không có hardcode API key trong source?
  • Đã chạy git log --all -S 'password' để check history chưa?

Nếu đã lỡ commit secret lên GitHub, xóa commit thôi chưa đủ - phải rotate key đó ngay lập tức vì GitHub scan history rất kỹ.


Tổng kết: bắt đầu từ đâu?

Mình biết đọc xong có thể overwhelmed - Docker, OIDC, Turborepo, Doppler... Nhưng không cần setup tất cả cùng một lúc.

Roadmap thực tế:

Tuần 1: Viết Dockerfile multi-stage cho project đang có. Build thử local, kiểm tra image size. Mục tiêu: dưới 200MB.

Tuần 2: Tạo workflow GitHub Actions đơn giản - chỉ chạy pnpm test khi có PR. Không cần deploy tự động ngay, quen flow đã.

Tuần 3: Kết nối workflow với Vercel deploy. Xóa access key cũ, chuyển sang OIDC nếu dùng AWS.

Tuần 4 trở đi: Nếu team lớn hơn 3 người, cân nhắc monorepo Turborepo và Doppler cho secrets.

343128 4 tuần từ zero đến CI/CD - không cần học tất cả cùng lúc

Nếu bạn mới bắt đầu với DevOps và muốn có nền tảng vững hơn về Linux và terminal - thứ bắt buộc khi làm việc với Docker và server - khóa Làm việc với Terminal & Ubuntu cover đúng những gì bạn cần trước khi đi sâu vào phần này. Và nếu cần học Next.js trước khi containerize, khóa Frontend NextJS là điểm bắt đầu tốt.

Gặp vấn đề gì trong quá trình setup, drop comment - mình đọc hết.