Docker Hóa Ứng Dụng và Đẩy Image lên ECR

K
Kai··5 min read

Trong bài này chúng ta đóng gói một ứng dụng vào Docker image rồi đẩy image đó lên ECR — kho chứa image của AWS. Đây là bước chuẩn bị cho Bài 7, nơi ta tự động hóa toàn bộ quá trình build và deploy.

Nhắc lại từ Bài 0: Docker đóng gói cả code lẫn môi trường vào một image, để ứng dụng chạy giống nhau ở mọi nơi. ECR (Elastic Container Registry) là nơi lưu các image đó trên AWS, để EC2 hay các dịch vụ khác kéo về chạy.

Mục tiêu

  1. Viết một ứng dụng Node.js nhỏ và một Dockerfile cho nó.
  2. Build image và chạy thử trên máy.
  3. Tạo một ECR repository.
  4. Đăng nhập ECR, tag và push image lên.
  5. Dọn dẹp repository.

Chi phí dự kiến

  • ECR tính tiền theo dung lượng image lưu trữ và lượng dữ liệu kéo ra. Một image Node.js nhỏ chỉ vài chục MB.
  • Tài khoản mô hình cũ (trước 15/07/2025): ECR miễn phí 500 MB lưu trữ mỗi tháng trong 12 tháng đầu.
  • Tài khoản mô hình credit: chi phí nhỏ này trừ vào credit.

Chi phí gần như không đáng kể, nhưng ta vẫn xóa repository ở cuối để giữ thói quen.

Bài này cần Docker cài sẵn trên máy. Kiểm tra bằng docker --version; nếu chưa có, cài Docker Desktop (macOS/Windows) hoặc Docker Engine (Linux).

Bước 1: Viết ứng dụng và Dockerfile

Tạo một thư mục dự án với một app Express đơn giản:

mkdir todo-app && cd todo-app

Tạo package.json:

{
  "name": "todo-app",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": { "start": "node server.js" },
  "dependencies": { "express": "^4.19.2" }
}

Tạo server.js:

const express = require("express");
const app = express();
const PORT = process.env.PORT || 3000;

app.get("/", (req, res) => {
  res.send("<h1>Todo app chay trong Docker container</h1>");
});

app.get("/health", (req, res) => {
  res.json({ status: "ok" });
});

app.listen(PORT, () => {
  console.log(`Server dang chay tren cong ${PORT}`);
});

Tạo Dockerfile:

# Dùng image Node chính thức làm nền
FROM node:20-alpine

# Thư mục làm việc bên trong container
WORKDIR /app

# Copy file khai báo dependency trước để tận dụng cache
COPY package.json ./
RUN npm install --omit=dev

# Copy phần code còn lại
COPY . .

# App lắng nghe cổng 3000
EXPOSE 3000

# Lệnh chạy khi container khởi động
CMD ["npm", "start"]

Thêm .dockerignore để không đóng gói các thứ thừa vào image:

node_modules
npm-debug.log
.git

Thứ tự trong Dockerfile có chủ đích: copy package.json và cài dependency trước, copy code sau. Docker cache theo từng bước, nên khi bạn chỉ sửa code mà không đổi dependency, bước npm install được dùng lại từ cache, build nhanh hơn.

Bước 2: Build và chạy thử trên máy

Build image, đặt tên todo-app:

docker build -t todo-app .

Chạy thử, ánh xạ cổng 3000 của container ra cổng 3000 của máy:

docker run -p 3000:3000 todo-app

Mở http://localhost:3000 trên trình duyệt, bạn thấy trang của app. Thử http://localhost:3000/health trả về JSON. Nhấn Ctrl+C ở terminal để dừng container.

Image chạy được trên máy thì cũng sẽ chạy y như vậy ở bất kỳ nơi nào có Docker, kể cả trên EC2.

Bước 3: Tạo ECR repository

Mỗi ứng dụng thường có một repository riêng trên ECR. Tạo repository tên todo-app:

aws ecr create-repository \
  --repository-name todo-app \
  --region ap-southeast-1 \
  --query "repository.repositoryUri" --output text

Lệnh trả về repository URI, dạng <account-id>.dkr.ecr.ap-southeast-1.amazonaws.com/todo-app. Lưu lại URI này. Lấy sẵn account id và đặt vài biến cho gọn:

ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION=ap-southeast-1
REPO_URI=$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/todo-app
echo $REPO_URI

Bước 4: Đăng nhập ECR và push image

ECR là registry riêng tư, nên Docker cần đăng nhập trước khi push. Lấy token đăng nhập từ AWS rồi đưa cho Docker:

aws ecr get-login-password --region $REGION \
  | docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com

Lệnh này lấy một token tạm (có hiệu lực 12 giờ) và đăng nhập Docker vào registry ECR của bạn. Khi thấy Login Succeeded là xong.

Image trên máy đang tên todo-app, nhưng để push lên ECR nó cần được tag theo đúng repository URI:

docker tag todo-app:latest $REPO_URI:latest

Push lên ECR:

docker push $REPO_URI:latest

Kiểm tra image đã có trên ECR:

aws ecr list-images --repository-name todo-app \
  --query "imageIds[].imageTag" --output table

Thấy tag latest trong danh sách nghĩa là image đã nằm trên ECR. Từ đây, EC2 hoặc các dịch vụ container của AWS có thể kéo image này về chạy — đó chính là cái Bài 7 sẽ tự động hóa.

Về tag latest: tiện cho lúc học, nhưng trong thực tế nên tag image theo phiên bản cụ thể (ví dụ theo commit hash của Git) để biết chính xác bản nào đang chạy và có thể quay lại bản cũ khi cần. Ta sẽ làm đúng kiểu này ở Bài 7.

🧹 Dọn dẹp

ECR tính tiền theo dung lượng image lưu trữ, nên xóa repository (kèm toàn bộ image bên trong):

aws ecr delete-repository \
  --repository-name todo-app \
  --region ap-southeast-1 \
  --force

--force cho phép xóa cả khi repository còn image bên trong. Kiểm tra đã xóa:

aws ecr describe-repositories \
  --query "repositories[].repositoryName" --output table

Trên máy, bạn có thể xóa các image Docker đã build để giải phóng ổ đĩa (không bắt buộc):

docker rmi todo-app $REPO_URI:latest

Nếu định làm Bài 7 ngay sau bài này, bạn có thể giữ lại thư mục todo-app và code, vì Bài 7 dùng lại chính ứng dụng này. Chỉ cần xóa ECR repository là đủ để không phát sinh chi phí; repository sẽ được tạo lại ở Bài 7.

Tổng kết

Bạn vừa đóng gói một ứng dụng vào Docker image, chạy thử, rồi đẩy nó lên ECR. Quy trình build → tag → push này là thao tác cốt lõi mà mọi pipeline CI/CD đều thực hiện, chỉ khác là tự động thay vì gõ tay.

Bài 7 ghép mọi thứ lại: thiết lập GitHub Actions để mỗi lần push code lên Git, hệ thống tự build image, push lên ECR và deploy lên EC2 — không còn thao tác thủ công nào.