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ị:
- Tài khoản GitHub: để quản lý mã nguồn của bạn.
- Tài khoản Docker Hub: để lưu trữ và chia sẻ các Docker images.
- 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.
- 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
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.
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
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
.
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.
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
.
/** @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.
docker build -t 200lab-nextjs .
Chạy câu lệnh sau để kiểm tra image vừa build được.
docker image ls | grep 200lab-nextjs
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.
docker run -it -d -p 3000:3000 --name 200lab-nextjs 200lab-nextjs
Kiểm tra container đã run thành công hay chưa.
docker ps | grep 200lab
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.
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é.
docker build --platform=linux/amd64,linux/arm64 -t nghiatran0502/200lab-nextjs .
Push image lên Docker Hub.
docker push nghiatran0502/200lab-nextjs
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.
curl -fsSL https://get.docker.com/ | sh
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh ./get-docker.sh --dry-run
Ở 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.
sudo usermod -aG docker $(whoami)
Reboot lại server của bạn
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.
docker pull nghiatran0502/200lab-nextjs
Kiểm tra image đã có trên server chưa.
docker image ls
Chạy container với image vừa được pull về.
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.
docker ps || grep 200lab-nextjs
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.
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.
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.
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_USERNAME
và DOCKER_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
.
Chọn New repository secret
=> điền thông tin Key
và Secret
(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.
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.
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.
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.
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).
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
.
Lên server và kiểm tra lại container
docker ps || grep 200lab-next
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.
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.
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.
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
Linux là gì? So sánh Hệ điều hành Linux và Windows
Oct 03, 2024 • 8 min read
Full bộ source code: Simple Task Microservices
Oct 02, 2024 • 1 min read
Fluentd là gì? So sánh Fluentd và Logstash
Sep 29, 2024 • 11 min read
Nginx là gì? Web Server đa năng cho các Hệ thống lớn
Sep 27, 2024 • 11 min read
Hướng dẫn Cài đặt Self-hosted Runners cho Github Actions
Sep 25, 2024 • 7 min read
Infrastructure as Code (IaC) là gì?
Sep 23, 2024 • 9 min read