Chào mừng trở lại với loạt bài viết “Roadmap Docker”! Sau khi đã cùng nhau khám phá Container là gì, phân biệt giữa Container, VM và Bare Metal, và tìm hiểu sâu hơn về Docker và Tiêu chuẩn OCI, cũng như trang bị các kỹ năng cơ bản về Linux và các lệnh Shell cần thiết, đã đến lúc chúng ta nâng cấp kỹ năng của mình lên một tầm cao mới: tự động hóa.
Trong thế giới DevOps bận rộn, việc lặp đi lặp lại các lệnh thủ công không chỉ tốn thời gian mà còn dễ gây ra lỗi. Đây chính là lúc Shell Script phát huy sức mạnh của mình, đặc biệt là khi làm việc với Docker. Shell Scripting cho phép chúng ta đóng gói các chuỗi lệnh Docker phức tạp thành những script đơn giản, dễ thực thi và tái sử dụng.
Mục lục
Tại Sao Shell Scripting Lại Mạnh Mẽ Khi Làm Việc Với Docker?
Docker được thiết kế để chạy trên nền tảng Linux, và Shell (như Bash, Zsh) là ngôn ngữ giao tiếp tự nhiên với nhân Linux. Điều này tạo nên một sự kết hợp hoàn hảo:
- Truy cập trực tiếp các lệnh Docker: Mọi lệnh
docker build
,docker run
,docker stop
, v.v., đều là các chương trình thực thi mà bạn có thể gọi trực tiếp từ Shell script. - Tính linh hoạt: Bạn có thể kết hợp các lệnh Docker với các lệnh Shell khác (như
grep
,awk
,sed
,if
,for
) để tạo ra logic phức tạp hơn, xử lý dữ liệu, kiểm tra điều kiện trước khi thực hiện hành động, v.v. - Phổ biến và dễ tiếp cận: Shell là môi trường mặc định trên hầu hết các hệ thống Linux và macOS, nơi Docker thường được triển khai. Bạn không cần cài đặt thêm công cụ phức tạp để bắt đầu viết script.
- Tự động hóa quy trình: Từ việc xây dựng image, chạy container, quản lý volume, mạng, đến việc dọn dẹp tài nguyên, tất cả đều có thể được tự động hóa chỉ bằng vài dòng Shell script.
Việc nắm vững Shell scripting sẽ giúp bạn không chỉ làm việc hiệu quả hơn với Docker mà còn xây dựng các pipeline CI/CD đơn giản, quản lý môi trường phát triển và sản xuất một cách nhất quán.
Những Lệnh Docker Cơ Bản Thường Được Tự Động Hóa
Hãy điểm qua một số tác vụ Docker phổ biến mà Shell script có thể giúp bạn tự động hóa:
- Xây dựng Image:
docker build
- Chạy Container:
docker run
- Dừng/Xóa Container:
docker stop
,docker rm
- Xóa Image:
docker rmi
- Quản lý Volume:
docker volume create
,docker volume rm
,docker volume prune
- Quản lý Network:
docker network create
,docker network rm
- Kiểm tra trạng thái:
docker ps
,docker images
,docker inspect
- Dọn dẹp tài nguyên:
docker system prune
Thay vì gõ đi gõ lại các lệnh này, chúng ta có thể nhóm chúng lại trong một script.
Ví Dụ Thực Tế: Xây Dựng và Chạy Ứng Dụng Web Đơn Giản
Giả sử bạn có một ứng dụng web đơn giản và muốn tự động hóa quá trình xây dựng image và chạy container.
Dockerfile: (Đặt tên là Dockerfile
)
# Sử dụng image nền
FROM ubuntu:latest
# Cập nhật hệ thống và cài đặt Nginx
RUN apt-get update && apt-get install -y nginx && rm -rf /var/lib/apt/lists/*
# Sao chép file index.html (giả định có sẵn) vào thư mục web root của Nginx
COPY index.html /var/www/html/
# Mở port 80
EXPOSE 80
# Lệnh chạy khi container khởi động
CMD ["nginx", "-g", "daemon off;"]
index.html: (Đặt tên là index.html
)
<!DOCTYPE html>
<html>
<head>
<title>Chào Mừng đến với Docker</title>
</head>
<body>
<h1>Ứng dụng Web Đơn Giản Chạy Trên Docker!</h1>
<p>Xin chào từ container Nginx!</p>
</body>
</html>
Script tự động hóa: (Đặt tên là deploy.sh
)
#!/bin/bash
# Tên image và container
IMAGE_NAME="my-nginx-app"
CONTAINER_NAME="nginx-web-container"
PORT=8080 # Port trên host
echo "--- Xây dựng Docker image: ${IMAGE_NAME} ---"
# docker build -t <tên_image> <đường_dẫn_Dockerfile>
docker build -t ${IMAGE_NAME} .
# Kiểm tra xem build có thành công không
if [ $? -ne 0 ]; then
echo "Lỗi khi xây dựng Docker image. Thoát."
exit 1
fi
echo "--- Dừng và xóa container cũ (nếu có): ${CONTAINER_NAME} ---"
# Kiểm tra xem container có đang chạy không
if docker ps -a --format '{{.Names}}' | grep -q ${CONTAINER_NAME}; then
echo "Container ${CONTAINER_NAME} đã tồn tại. Đang dừng..."
docker stop ${CONTAINER_NAME} > /dev/null 2>&1
echo "Đang xóa container ${CONTAINER_NAME}..."
docker rm ${CONTAINER_NAME} > /dev/null 2>&1
echo "Container cũ đã xóa."
fi
echo "--- Chạy container mới: ${CONTAINER_NAME} ---"
# docker run -d -p <port_host>:<port_container> --name <tên_container> <tên_image>
docker run -d -p ${PORT}:80 --name ${CONTAINER_NAME} ${IMAGE_NAME}
# Kiểm tra xem run có thành công không
if [ $? -ne 0 ]; then
echo "Lỗi khi chạy Docker container. Thoát."
exit 1
fi
echo "--- Ứng dụng đã được triển khai ---"
echo "Container ${CONTAINER_NAME} đang chạy, ánh xạ port ${PORT} trên host tới port 80 trong container."
echo "Bạn có thể truy cập ứng dụng tại http://localhost:${PORT}"
# Hiển thị trạng thái container
docker ps --filter "name=${CONTAINER_NAME}"
Để chạy script này, lưu ba file trên vào cùng một thư mục, mở terminal tại thư mục đó và chạy:
chmod +x deploy.sh
./deploy.sh
Script này thực hiện tuần tự các bước:
- Định nghĩa các biến (tên image, tên container, port).
- Xây dựng Docker image từ Dockerfile trong thư mục hiện tại.
- Kiểm tra mã thoát của lệnh
docker build
. Nếu khác 0 (lỗi), in thông báo và thoát. - Kiểm tra xem container với tên đã định nghĩa có tồn tại không.
- Nếu có, dừng và xóa container cũ.
- Chạy container mới từ image vừa xây dựng, ánh xạ port 8080 trên host tới port 80 trong container.
- Kiểm tra mã thoát của lệnh
docker run
. Nếu khác 0, in thông báo và thoát. - In thông báo thành công và thông tin truy cập.
- Hiển thị trạng thái của container vừa chạy.
Script này sử dụng các khái niệm Shell cơ bản như biến ($VAR_NAME
), kiểm tra điều kiện (if
, grep -q
, kiểm tra mã thoát $?
), và chuyển hướng output (> /dev/null 2>&1
để ẩn output thành công của stop
/rm
). Đây là những kỹ năng cốt lõi mà bạn đã được làm quen trong bài viết về Các Lệnh Shell Cần Thiết Cho Người Dùng Docker.
Tự động hóa Tác vụ Dọn dẹp Tài nguyên Docker
Một tác vụ quan trọng khác trong quy trình làm việc với Docker là quản lý tài nguyên và dọn dẹp rác (các container dừng, image không sử dụng, volume và mạng không còn liên kết). Lệnh docker system prune
rất hữu ích, nhưng bạn có thể muốn tùy chỉnh hoặc thực hiện dọn dẹp định kỳ. Shell script là lựa chọn lý tưởng.
Script dọn dẹp: (Đặt tên là cleanup_docker.sh
)
#!/bin/bash
echo "--- Bắt đầu dọn dẹp tài nguyên Docker ---"
# Dừng tất cả các container đang chạy
echo "Đang dừng tất cả các container đang chạy..."
docker stop $(docker ps -q) > /dev/null 2>&1 || true
# Xóa tất cả các container dừng
echo "Đang xóa tất cả các container dừng..."
docker rm $(docker ps -a -q) > /dev/null 2>&1 || true
# Xóa tất cả các image không sử dụng (dangling images)
echo "Đang xóa tất cả các image không sử dụng..."
docker rmi $(docker images -f "dangling=true" -q) > /dev/null 2>&1 || true
# Xóa tất cả các volume không sử dụng
echo "Đang xóa tất cả các volume không sử dụng..."
docker volume prune -f > /dev/null 2>&1 || true # -f để không hỏi xác nhận
# Xóa tất cả các mạng không sử dụng
echo "Đang xóa tất cả các mạng không sử dụng..."
docker network prune -f > /dev/null 2>&1 || true # -f để không hỏi xác nhận
# Có thể thêm docker system prune cho dọn dẹp toàn diện hơn, nhưng cẩn thận
# echo "Đang chạy docker system prune..."
# docker system prune -a -f --volumes
echo "--- Dọn dẹp hoàn tất ---"
Chạy script này cần quyền thực thi:
chmod +x cleanup_docker.sh
./cleanup_docker.sh
Script này sử dụng:
- Command substitution
$(...)
để lấy output của một lệnh (ví dụ: danh sách ID container) và sử dụng nó làm đối số cho lệnh khác. || true
để đảm bảo script không dừng lại nếu lệnhdocker stop
hoặcdocker rm
không tìm thấy container nào để xử lý (lệnhdocker ps -q
trả về rỗng).- Các flag
-f
choprune
để bỏ qua bước xác nhận, phù hợp cho tự động hóa.
Bạn có thể lên lịch chạy script này định kỳ bằng cron
trên Linux để giữ cho hệ thống Docker của mình luôn gọn gàng.
Sử Dụng Biến, Điều kiện và Vòng lặp trong Script Docker
Để làm cho script của bạn linh hoạt và mạnh mẽ hơn, hãy kết hợp các cấu trúc Shell script nâng cao.
Biến: Như đã thấy ở ví dụ trên, sử dụng biến (IMAGE_NAME="my-app"
, docker build -t ${IMAGE_NAME} .
) giúp script dễ đọc, dễ sửa đổi và tránh gõ nhầm.
Đối số đầu vào: Bạn có thể truyền thông tin vào script từ bên ngoài sử dụng $1
, $2
, …
#!/bin/bash
IMAGE_NAME="$1" # Lấy tên image từ đối số đầu tiên
VERSION="$2" # Lấy version từ đối số thứ hai
if [ -z "$IMAGE_NAME" ] || [ -z "$VERSION" ]; then
echo "Cách dùng: $0 <image_name> <version>"
exit 1
fi
FULL_IMAGE_TAG="${IMAGE_NAME}:${VERSION}"
echo "Đang xây dựng image: ${FULL_IMAGE_TAG}"
docker build -t ${FULL_IMAGE_TAG} .
Chạy script này: ./build_image.sh my-web-app 1.2.0
Kiểm tra điều kiện (`if`): Rất hữu ích để kiểm tra xem một container đã chạy chưa, một image có tồn tại không, hoặc một biến đã được đặt giá trị chưa, trước khi thực hiện hành động tiếp theo.
# Kiểm tra xem image có tồn tại không trước khi push
IMAGE="my-app:latest"
if docker image inspect ${IMAGE} > /dev/null 2>&1; then
echo "Image ${IMAGE} tồn tại. Đang push..."
docker push ${IMAGE}
else
echo "Image ${IMAGE} không tồn tại. Không thể push."
fi
Vòng lặp (`for`, `while`): Tuyệt vời để xử lý danh sách các container, image, hoặc thực hiện cùng một hành động trên nhiều mục.
# Dừng và xóa nhiều container theo tên
CONTAINERS_TO_STOP=("web-server" "db-server" "cache-server")
for container_name in "${CONTAINERS_TO_STOP[@]}"; do
echo "Đang xử lý container: ${container_name}"
if docker ps -a --format '{{.Names}}' | grep -q "${container_name}"; then
echo " Đang dừng ${container_name}..."
docker stop "${container_name}" > /dev/null 2>&1 || true
echo " Đang xóa ${container_name}..."
docker rm "${container_name}" > /dev/null 2>&1 || true
else
echo " Container ${container_name} không tồn tại."
fi
done
Bằng cách kết hợp các cấu trúc này, bạn có thể tạo ra các script phức tạp hơn, đáp ứng nhu cầu tự động hóa đa dạng.
Xử lý Lỗi trong Shell Script Docker
Trong môi trường tự động hóa, việc xử lý lỗi là cực kỳ quan trọng để script không chạy sai hoặc gây hậu quả không mong muốn khi gặp vấn đề. Các kỹ thuật phổ biến bao gồm:
- Kiểm tra mã thoát (`$?`): Mỗi lệnh thực thi trong Shell trả về một mã thoát (exit code). 0 thường là thành công, khác 0 là lỗi. Bạn nên kiểm tra mã này sau các lệnh Docker quan trọng.
- Sử dụng `set -e`: Đặt ở đầu script. Lệnh này khiến script tự động thoát ngay lập tức nếu bất kỳ lệnh nào trả về mã thoát khác 0. Điều này ngăn chặn script tiếp tục chạy với trạng thái không mong muốn sau khi một bước quan trọng bị lỗi.
- Sử dụng `||`: Kết hợp với một lệnh khác để xử lý lỗi. Ví dụ:
docker build || echo "Build lỗi!"
sẽ in thông báo “Build lỗi!” nếudocker build
thất bại. - Ghi log: Chuyển hướng output và error output của script vào file log để dễ dàng kiểm tra khi có sự cố.
Ví dụ với set -e
:
#!/bin/bash
set -e # Thoát script ngay nếu có lệnh lỗi
IMAGE_NAME="my-faulty-app"
echo "--- Xây dựng image ---"
# Lệnh build này có thể lỗi nếu Dockerfile sai cú pháp hoặc thiếu file
docker build -t ${IMAGE_NAME} .
# Nếu lệnh build ở trên lỗi, script sẽ dừng tại đây nhờ set -e
echo "--- Push image (sẽ không chạy nếu build lỗi) ---"
docker push ${IMAGE_NAME}
echo "--- Hoàn tất quy trình ---" # Dòng này chỉ in ra nếu tất cả các lệnh trước thành công
Việc thêm set -e
là một best practice tốt cho hầu hết các script tự động hóa Docker.
So Sánh: Thủ Công vs. Tự động hóa với Script
Hãy xem một bảng so sánh đơn giản về cách thực hiện một số tác vụ phổ biến:
Tác vụ | Thủ Công (Lệnh Shell Trực Tiếp) | Tự động hóa (Script Shell) | Ưu điểm của Script |
---|---|---|---|
Xây dựng image | docker build -t my-app:latest . |
Viết script sử dụng biến, kiểm tra lỗi:
|
Nhất quán, ít gõ nhầm, kiểm tra lỗi tự động, dễ thay đổi tên/version. |
Chạy container | docker run -d -p 8080:80 --name my-web my-app:latest |
Viết script kiểm tra container cũ, dừng/xóa, chạy mới với biến:
|
Đảm bảo container cũ được xử lý, dễ dàng thay đổi port/tên, quy trình triển khai lặp lại. |
Dọn dẹp container dừng | docker ps -a -q -f "status=exited" | xargs docker rm |
Script dọn dẹp với nhiều bước, có thể lên lịch chạy:
|
Tự động, đảm bảo không bỏ sót, có thể lên lịch chạy định kỳ. |
Rõ ràng, đối với các tác vụ lặp đi lặp lại hoặc cần nhiều bước, tự động hóa bằng Shell script mang lại hiệu quả và độ tin cậy cao hơn đáng kể.
Best Practices khi Viết Shell Script cho Docker
- Luôn bắt đầu script với Shebang:
#!/bin/bash
(hoặc#!/bin/sh
nếu bạn muốn tương thích cao hơn). - Sử dụng
set -e
: Giúp phát hiện lỗi sớm. - Sử dụng biến: Cho các giá trị dễ thay đổi như tên image, version, port, tên container, v.v.
- Thêm comment: Giải thích logic phức tạp hoặc các phần quan trọng.
- Xử lý lỗi rõ ràng: Kiểm tra mã thoát, sử dụng
|| true
khi cần. - Đảm bảo script là Idempotent (có thể chạy nhiều lần mà kết quả vẫn giống nhau): Ví dụ: script chạy container nên kiểm tra và xóa container cũ trước khi chạy container mới.
- Sử dụng tên container/network rõ ràng: Tránh nhầm lẫn.
- Tránh sử dụng quyền root không cần thiết: Chạy lệnh Docker với người dùng có quyền phù hợp.
- Kiểm tra script trong môi trường non-production trước: Luôn thử nghiệm script của bạn trước khi áp dụng cho môi trường quan trọng.
- Sử dụng Docker Compose cho các ứng dụng phức tạp: Đối với các ứng dụng có nhiều service liên kết với nhau, Docker Compose là công cụ chuyên biệt và phù hợp hơn Shell script. Shell script thường tốt cho các tác vụ đơn lẻ, tùy chỉnh, hoặc các bước trong pipeline CI/CD.
Kết luận
Shell scripting là một kỹ năng vô giá cho bất kỳ DevOps Engineer nào làm việc với Docker. Nó không chỉ giúp bạn tiết kiệm thời gian bằng cách tự động hóa các tác vụ lặp đi lặp lại mà còn giảm thiểu lỗi do con người gây ra và xây dựng các quy trình làm việc nhất quán, đáng tin cậy.
Bằng cách kết hợp kiến thức về Linux cơ bản, các lệnh Shell thiết yếu với các lệnh Docker, bạn có thể tạo ra các script mạnh mẽ để quản lý vòng đời của container, từ xây dựng image, triển khai ứng dụng, đến quản lý tài nguyên và dọn dẹp hệ thống.
Hãy bắt tay vào thực hành viết script tự động hóa cho các quy trình Docker hàng ngày của bạn. Bắt đầu với những script đơn giản, sau đó dần dần thêm các cấu trúc điều khiển, xử lý lỗi và biến để làm cho chúng linh hoạt hơn. Đây là một bước tiến quan trọng trên con đường trở thành một DevOps Engineer chuyên nghiệp.
Hẹn gặp lại các bạn trong các bài viết tiếp theo của “Roadmap Docker”!