Facebook Pixel

Hướng dẫn triển khai CI/CD cho ứng dụng NextJS sử dụng Github Actions

17 Sep, 2024

Chúng ta sẽ thực hành xây dựng quy trình CI/CD cho một ứng dụng NextJS cơ bản và triển khai lên server bằng cách sử dụng Github Actions

Hướng dẫn triển khai CI/CD cho ứng dụng NextJS sử dụng Github Actions

Mục Lục

Trong bài viết này, chúng ta sẽ thực hành xây dựng quy trình CI/CD cho một ứng dụng NextJS cơ bản và triển khai lên server bằng cách sử dụng Github Actions, phần server chúng ta sẽ sử dụng dịch vụ EC2 của AWS.

Mình sẽ chia nhỏ bài hướng dẫn thành các đầu mục như sau:

  • Cài đặt ứng dụng NextJS cơ bản
  • Viết Dockerfile, kiểm tra dưới máy Local và push image lên Docker Hub
  • Cài đặt Server
  • Build CICD cho ứng dụng này

1. Yêu cầu

Trước tiên, đây là những thứ bạn cần chuẩn bị:

  1. Tài khoản GitHub: để quản lý mã nguồn của bạn.
  2. Tài khoản Docker Hub: để lưu trữ và chia sẻ các Docker images.
  3. Kiến thức cơ bản về Docker: cần phải có để có thể thao tác được với docker và viết docker file cho dự án.
  4. Linux Server: để triển khai ứng dụng của bạn.

2. Khởi tạo ứng dụng NextJS cơ bản

Ở phần này mình sẽ không đi vào chi tiết mà mình chỉ cài đặt cơ bản và thực hiện trên Base Project của NextJS.

2.1 Cài đặt

Bash
npx create-next-app@latest

Chạy đoạn script bên trên chúng ta sẽ được kết quả giống hình bên dưới.

Cai-dat-nextjs
Cài đặt NextJS

2.2 Test ứng dụng

Tiếp đến, chạy đoạn script bên dưới để khởi chạy ứng dụng

Bash
yarn dev

Ở đây thì mình đang dùng yarn, các bạn có thể sử dụng npm, pnpm tuỳ theo nhu cầu của các bạn. Sau khi chạy yarn dev thì chúng ta sẽ tạo được một server NextJS chạy dưới máy local của chúng ta. Mặc định thì sẽ là port 3000.

ung-dung-nextjs
Ứng dụng NextJS

Vậy là chúng ta đã có một ứng dụng NextJS cơ bản, tiếp theo chúng ta sẽ dockerize ứng dụng này.

3. Viết Dockerfile và Push Image lên Docker Hub

Trong phần này, chúng ta sẽ tạo Dockerfile để build ứng dụng thành image và đẩy lên Docker Hub.

3.1 Viết Dockerfile

Về phần Dockerfile mình sẽ sử dụng file example của NextJS.

Dockerfile
FROM node:18-alpine AS base

# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
  else echo "Lockfile not found." && exit 1; \
  fi


# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED=1

RUN \
  if [ -f yarn.lock ]; then yarn run build; \
  elif [ -f package-lock.json ]; then npm run build; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
  else echo "Lockfile not found." && exit 1; \
  fi

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

ENV NODE_ENV=production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED=1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT=3000

# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

3.2 Cập nhật Next config

Để run được Dockerfile, các bạn cần cập nhật lại một chút ở file next.config.mjs. Thêm Key output vào Object nextConfig.

Javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: "standalone",
};

export default nextConfig;

Việc này sẽ tạo thư mục .next/standalone khi build và sau đó chúng ta có thể deploy ứng dụng của mình mà không cần cài đặt node_modules.

3.3 Build ứng dụng thành Image

Ở bước này, chúng ta sẽ build ứng dụng thành image với tên là 200lab-nextjs bằng cách chạy câu lệnh bên dưới.

Bash
docker build -t 200lab-nextjs .
build-nextjs-to-docker-image
Build ứng dụng nextjs thành image

Chạy câu lệnh sau để kiểm tra image vừa build được.

Bash
docker image ls | grep 200lab-nextjs
docker-image
Docker Image đã được build thành công

Vậy là chúng ta đã build thành công Docker Image với metadata như sau:

  • Tên: 200lab-nextjs
  • Version: lastest
  • Size: 210MB

Run docker container với 200lab-nextjs image mà ta vừa build.

Bash
 docker run -it -d -p 3000:3000 --name 200lab-nextjs 200lab-nextjs  

Kiểm tra container đã run thành công hay chưa.

Bash
docker ps | grep 200lab
docker-container
Container đã được khởi chạy thành công
run-container
Khởi chạy container thành công

3.4 Push image vừa build lên Docker Hub để lưu trữ

Đầu tiên, các bạn cần phải đăng nhập vào Docker Hub.

Bash
docker login

Build lại image với username docker hub của các bạn, các bạn nhớ thay bằng docker hub username của các bạn nhé (của mình là nghiatran0502). Không push bằng username của mình được đâu nhé.

Bash
docker build --platform=linux/amd64,linux/arm64 -t nghiatran0502/200lab-nextjs .

Push image lên Docker Hub.

Bash
docker push nghiatran0502/200lab-nextjs
push-image-to-docker-hub
Push image lên docker hub
docker-hub
Image đã được push lên thành công

Như vậy, chúng ta đã có sẵn image trên Docker Hub và nắm được logic cơ bản về việc build và deploy một ứng dụng NextJS. Tiếp theo, chúng ta sẽ thiết lập server và cài đặt các môi trường cần thiết để có thể chạy ứng dụng trên server.

4. Cài đặt Server EC2

Trong phần này, mình sẽ không hướng dẫn cách khởi tạo server mà sẽ đi thẳng vào việc cài đặt môi trường và chạy dự án NextJS trên server. Server mà chúng ta sử dụng là EC2 của AWS, vì ứng dụng đã được dockerize, nên chúng ta sẽ cần cài Docker trên server để build image thành container. Bạn cũng cần SSH Key để truy cập vào server.

4.1 Cài đặt docker

Chạy các câu lệnh bên dưới để cài đặt Docker trên server EC2, hệ điều hành mình đang sử dụng là Ubuntu.

Bash
curl -fsSL https://get.docker.com/ | sh
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh ./get-docker.sh --dry-run
install-docker-success
Cài đặt thành công

Ở bước này, bạn đã cài đặt Docker trên server, nhưng khi chạy lệnh docker ps, bạn sẽ gặp vấn đề về quyền truy cập. Để chạy lệnh mà không cần dùng sudo, bạn có thể sử dụng đoạn script dưới đây.

Bash
sudo usermod -aG docker $(whoami)

Reboot lại server của bạn

Bash
sudo reboot

4.2 Khởi tạo Docker Conainer

Đầu tiên, chúng ta sẽ pull image mà ta đã lưu trữ trên Docker Hub về lại server.

Bash
docker pull nghiatran0502/200lab-nextjs

Kiểm tra image đã có trên server chưa.

Bash
docker image ls

Chạy container với image vừa được pull về.

Bash
docker run -it -d --name 200lab-next -p 3000:3000 nghiatran0502/200lab-nextjs

Kiểm tra container có running thành công hay chưa.

Bash
docker ps || grep 200lab-nextjs
docker-ps
Container đã được khởi chạy thành công

Bây giờ các bạn có thể truy cập trong web của mình bằng [IP_server]:[PORT]. Lưu ý là trên EC2, bạn cần mở port 3000 vì đây là cổng mà ứng dụng Next.js thường sử dụng để chạy. Nếu không mở cổng này trong Security Group của EC2, các yêu cầu từ bên ngoài sẽ không thể truy cập vào ứng dụng, dẫn đến việc không thể truy cập trang web qua địa chỉ IP và cổng đã chỉ định.

Deploy-success
Deploy thành công

Vậy là chúng ta đã có thể deploy ứng dụng NextJS lên server thành công. Nhưng hiện tại thì chúng ta đang phải thao tác tay quá nhiều, bây giờ chúng ta sẽ bắt đầu tự động hoá quá trình build và deploy ứng dụng thông qua Github Action.

5. Thiết lập quy trình CI/CD bằng Github Actions

Ở phần này thì chúng ta cũng chỉ sẽ thực hành build CI/CD cơ bản sử dụng Github Actions.

5.1 Push code lên Github repository

Vì chúng ta sẽ sử dụng Github Actions để thiết lập quy trình CI/CD, nên bước đầu tiên là phải đảm bảo source code đã được đẩy lên Github.

github-action
Ứng dụng nextjs được lưu trữ trên github

4.2 Build và Push image lên Docker Hub

Trong thư mục gốc (root) của dự án, chúng ta cần tạo một thư mục mới có tên .github/workflows. Đây là nơi lưu trữ các workflow mà Github Actions sẽ sử dụng để tự động hóa quá trình build và deploy.

Tiếp theo, bạn tạo một file có tên deploy.yml bên trong thư mục vừa tạo. File này sẽ chứa các bước cấu hình để build image của ứng dụng và đẩy lên Docker Hub, đảm bảo quá trình triển khai diễn ra liên tục mỗi khi có thay đổi trong repository.

Yml
name: "Build and deploy to server"

on:
  push:
    # Chúng ta sẽ chạy khi chúng ta release một version mới
    tags:
      - "v*"

jobs:
  deploy:
    name: Deploy to server
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Login to docker hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Tags docker image
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ secrets.DOCKER_USERNAME }}/200lab-nextjs # Change this to your docker image name

      - name: Build and push to docker hub
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}

Lưu ý rằng trong file deploy.yml, chúng ta sẽ cần sử dụng các thông tin nhạy cảm như DOCKER_USERNAMEDOCKER_PASSWORD. Các bạn tuyệt đối không nên viết trực tiếp những thông tin này vào file, vì việc này có thể làm lộ tài khoản và gây rủi ro bảo mật. Thay vào đó, chúng ta sẽ sử dụng tính năng secrets của Github để bảo vệ và quản lý các thông tin nhạy cảm này.

Truy cập lại vào repository trên Github, chọn Setting -> Secrets and variables -> Actions.

github-secrets-and-variables
Github Secrets and variables 

Chọn New repository secret => điền thông tin KeySecret (value) vào đây, chúng ta có bao nhiêu Secret thì chúng ta tạo bấy nhiêu. Lưu ý, với DOCKER_PASSWORD chúng ta sẽ dùng chuỗi secret của docker hub thay cho mật khẩu.

new-github-secret
New Github Secret
github-secret
Github Secrets

Như các bạn đã thấy thì mình đã tạo được 2 key chứa thông tin username và password cho tài khoản Docker Hub của mình.

5.3 Thực thi Github Actions đã thiết lập

Bạn truy cập vào Github repository -> Create a new release,  tạo tag v0.0.1-beta.1 (bạn có thể tùy chỉnh tên phiên bản, nhưng cần tuân theo định dạng v.*) -> Publish release.

Tiếp theo, chuyển sang tab Actions, bạn sẽ thấy Github Actions của chúng ta tự động bắt đầu chạy. Đây là bước giúp kích hoạt quy trình CI/CD mà chúng ta đã thiết lập, đảm bảo việc build và deploy diễn ra ngay sau khi phiên bản mới được phát hành.

github-action
Github Action
github-action
Github Action

Bạn có thể theo dõi tiến trình của từng job trong quy trình CI/CD. Tại đây, bạn sẽ thấy chi tiết từng bước mà chúng ta đã cấu hình, từ build image đến push lên Docker Hub.

github-action-detail
Github Action Detail

Truy cập lại vào Docker hub kiểm tra thì các bạn sẽ thấy Image của mình vừa build với tags v0.0.1-beta.1 đã được push lên thành công.

docker-hub
Docker Hub

Như vậy là chúng ta đã tự động hoá thành công bước build và push image của chúng ta lên Docker Hub thành công. Bây giờ chúng ta sẽ tự động hoá quá trình deploy lên server và khởi chạy với image mà chúng ta vừa build đó.

5.4 Khởi chạy Container trên Server

Cập nhật lại file deploy.yml với nội dung như sau (nhớ push code lại nha).

Yml
name: "Build and deploy to server"

on:
  push:
    # Chúng ta sẽ chạy khi chúng ta release một version mới
    tags:
      - "v*"

jobs:
  deploy:
    name: Deploy to server
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Login to docker hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Tags docker image
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ secrets.DOCKER_USERNAME }}/200lab-nextjs # Change this to your docker image name

      - name: Build and push to docker hub
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}

      - name: Deploy to server
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.HOST }} # Địa chỉ của server
          username: ${{ secrets.USERNAME }} # Username để login vào server
          key: ${{ secrets.SSH_KEY }} # Private key để login vào server

          script: |
#           Pull image về lại server
            docker pull ${{ steps.meta.outputs.tags }}
#           Xóa container cũ nếu có
            docker rm -f 200lab-next &>/dev/null
#           Chạy container mới
            docker run -it -d --name 200lab-next -p 3000:3000 ${{ steps.meta.outputs.tags }}

Trong phần này, chúng ta sẽ thêm 3 secrets mới: HOST, USERNAME, và SSH_KEY. Bạn có thể thêm các secrets này theo hướng dẫn đã nêu ở trên để đảm bảo bảo mật. Sau đó, tiến hành tạo lại một phiên bản release mới để kích hoạt lại Github Actions.

Vào GitHub repository => Create a new release. Tạo tag v0.0.1-beta.4 => Publish release.

deploy-success
Deploy success

Lên server và kiểm tra lại container

Bash
docker ps || grep 200lab-next
container-run-success
Container run success

Vậy là chúng ta đã thiết lập thành công một luồng CI/CD cơ bản để tự động deploy ứng dụng NextJS lên server. Giờ đây, mỗi khi bạn thay đổi code và tạo bản release mới, mọi quy trình từ build đến deploy sẽ diễn ra tự động mà bạn không cần phải thực hiện thủ công từng bước như trước.

Tiếp theo, mình sẽ cập nhật file page.tsx và tạo một phiên bản release mới để chứng minh cách hệ thống tự động hoạt động.

TSX
export default function Home() {
  return (
    <main>
        <h1>Welcome to 200lab</h1>
    </main>
  );
}

Việc bây giờ của mình bây giờ là ngồi đợi thôi.

action-running
Action Running

Và đây là kết quả nhận được khi mình truy cập vào ứng dụng NextJS trên server EC2.

success
Success

6. Kết luận

Trong bài viết này, mình đã hướng dẫn các bạn cách xây dựng một luồng CI/CD cơ bản để tự động triển khai ứng dụng NextJS lên server. Qua đó, chúng ta đã thấy cách tự động hóa quá trình build và deploy giúp tiết kiệm thời gian và giảm bớt các thao tác thủ công.

Trong bài viết tiếp theo, mình sẽ hướng dẫn các bạn cách sử dụng self-hosted runner trên Github Actions.

Các bài viết liên quan:

Bài viết liên quan

Lập trình backend expressjs

xây dựng hệ thống microservices
  • Kiến trúc Hexagonal và ứng dụngal font-
  • TypeScript: OOP và nguyên lý SOLIDal font-
  • Event-Driven Architecture, Queue & PubSubal font-
  • Basic scalable System Designal font-

Đăng ký nhận thông báo

Đừng bỏ lỡ những bài viết thú vị từ 200Lab