BÀI VIẾT // [DEVOPS] Bash và Docker trong DevOps: Quản lý container hiệu quả
Bash và Docker trong DevOps: Quản lý container hiệu quả

Bash và Docker trong DevOps: Quản lý container hiệu quả

Hướng dẫn kết hợp Bash và Docker trong DevOps: wrapper script, quản lý container, dọn image cũ, healthcheck, deploy stack với docker compose và tự động restart khi crash.

DEVOPS BASH

Bash và Docker trong DevOps

Trong bài viết trước, chúng ta đã tìm hiểu cách tự động hóa SSH để quản lý nhiều server song song. Bây giờ, chúng ta sẽ bước vào thế giới container — nơi Bash đóng vai trò như một bridge mạnh mẽ giữa bạn và Docker daemon, giúp bạn quản lý container một cách linh hoạt hơn bất kỳ UI nào.

Docker CLI đã rất mạnh mẽ, nhưng khi bạn cần chạy hàng loạt container, kiểm tra health status định kỳ, dọn dẹp image cũ, hay deploy một stack phức tạp — việc gõ lệnh từng cái một không còn thực tế. Bash cho phép bạn viết wrapper script để tự động hóa toàn bộ vòng đời container: từ build, deploy, healthcheck, restart khi crash, đến cleanup resource.

Bài viết này sẽ hướng dẫn bạn các kỹ thuật kết hợp Bash và Docker, từ những lệnh cơ bản đến một script thực hành quản lý container hoàn chỉnh.


Lệnh Docker cơ bản trong Bash

Liệt kê và lọc container

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Liệt kê tất cả container đang chạy
docker ps

# Liệt kê cả container đã dừng
docker ps -a

# Chỉ lấy container ID (phù hợp cho script)
docker ps -q

# Lọc theo tên
docker ps --filter "name=nginx"

# Lọc theo trạng thái
docker ps --filter "status=running"
docker ps --filter "status=exited"

# Kết hợp filter và format output
docker ps --format "{{.ID}} {{.Names}} {{.Status}} {{.Ports}}"

Kiểm tra trạng thái container

1
2
3
4
5
6
7
8
# Kiểm tra container có đang chạy không (trả về 0 nếu có)
docker inspect --format='{{.State.Running}}' myapp

# Lấy trạng thái chi tiết
docker inspect --format='{{.State.Status}}' myapp

# Kiểm tra health status (nếu có healthcheck)
docker inspect --format='{{.State.Health.Status}}' myapp

Quản lý container

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Khởi động container đã dừng
docker start myapp

# Dừng container
docker stop myapp

# Restart container
docker restart myapp

# Xóa container đã dừng
docker rm myapp

# Force remove (kể cả khi đang chạy)
docker rm -f myapp

Docker Compose

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Khởi động stack
docker compose up -d

# Dừng stack
docker compose down

# Xem log
docker compose logs -f

# Restart một service
docker compose restart nginx

Wrapper script cho Docker

Hàm tiện ích

Trước khi viết script phức tạp, hãy tạo một số hàm tiện ích để tái sử dụng:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/usr/bin/env bash

# Kiểm tra Docker daemon có chạy không
docker_ready() {
    docker info >/dev/null 2>&1
}

# Lấy ID của container theo tên, trả về rỗng nếu không tồn tại
container_id() {
    docker ps -q --filter "name=$1"
}

# Kiểm tra container có đang chạy không
container_running() {
    [[ -n "$(container_id "$1")" ]]
}

# Kiểm tra container có tồn tại (dù đã dừng) không
container_exists() {
    docker ps -a -q --filter "name=$1" | grep -q .
}

# Lấy logs của container (số dòng gần nhất)
container_logs() {
    local name="${1:?Container name required}"
    local lines="${2:-50}"
    docker logs --tail "$lines" "$name" 2>&1
}

Dọn dẹp Docker resource

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Xóa container đã dừng
cleanup_containers() {
    local count
    count=$(docker ps -a -q --filter "status=exited" | wc -l)
    if [[ "$count" -gt 0 ]]; then
        echo "Cleaning up $count stopped containers..."
        docker ps -a -q --filter "status=exited" | xargs docker rm -f
    fi
}

# Xóa dangling image (image không gắn với container nào)
cleanup_images() {
    local count
    count=$(docker images -f "dangling=true" -q | wc -l)
    if [[ "$count" -gt 0 ]]; then
        echo "Cleaning up $count dangling images..."
        docker image prune -f
    fi
}

# Xóa tất cả unused resource (cẩn thận!)
cleanup_all() {
    docker system prune -f --volumes
}

Healthcheck container với Bash

Khi container không có Docker healthcheck tích hợp sẵn, bạn có thể tự viết script kiểm tra bằng Bash:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/usr/bin/env bash
# Kiểm tra HTTP endpoint của container

check_http() {
    local container_name="${1:?Container name required}"
    local port="${2:-80}"
    local path="${3:-/}"
    local expected_status="${4:-200}"

    local ip
    ip=$(docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$container_name" 2>/dev/null)

    if [[ -z "$ip" ]]; then
        echo "[ERROR] Cannot get IP for container $container_name"
        return 1
    fi

    local status
    status=$(curl -s -o /dev/null -w "%{http_code}" "http://${ip}:${port}${path}" 2>/dev/null || echo "000")

    if [[ "$status" == "$expected_status" ]]; then
        echo "[OK] $container_name HTTP check passed (status: $status)"
        return 0
    else
        echo "[WARN] $container_name HTTP check failed (status: $status, expected: $expected_status)"
        return 1
    fi
}

# Kiểm tra nhiều containers
check_http "nginx" 80 "/" "200"
check_http "api" 8080 "/health" "200"

Deploy stack với Bash

Khi deploy một Docker Compose stack, bạn thường cần: pull image mới, dừng service cũ, khởi động lại, và kiểm tra status. Bash giúp bạn orchestrate toàn bộ quá trình này:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#!/usr/bin/env bash
# deploy-stack.sh — Deploy Docker Compose stack với healthcheck

set -euo pipefail

readonly COMPOSE_FILE="${1:-docker-compose.yml}"
readonly PROJECT_NAME="${2:-myapp}"
readonly HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-60}"
readonly LOG_FILE="/var/log/docker-deploy.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}

# Pull image mới nhất
pull_images() {
    log "Pulling latest images..."
    docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" pull
}

# Deploy stack
deploy_stack() {
    log "Deploying stack..."
    docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" up -d --remove-orphans
}

# Đợi container healthy
wait_healthy() {
    local service="$1"
    local elapsed=0

    log "Waiting for $service to become healthy..."
    while [[ $elapsed -lt $HEALTH_TIMEOUT ]]; do
        local status
        status=$(docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" ps --format json "$service" 2>/dev/null | grep -o '"Health":"[^"]*"' | cut -d'"' -f4)

        if [[ "$status" == "healthy" ]]; then
            log "$service is healthy"
            return 0
        fi

        sleep 2
        elapsed=$((elapsed + 2))
    done

    log "[WARN] $service did not become healthy within ${HEALTH_TIMEOUT}s"
    return 1
}

# Rollback khi deploy thất bại
rollback() {
    log "[ERROR] Deployment failed — rolling back..."
    docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" down
    # Có thể restore từ backup ở đây
}

trap rollback ERR

main() {
    log "=== Deployment started ==="

    pull_images
    deploy_stack

    # Đợi tất cả service healthy
    local services
    services=$(docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" ps --services)
    for svc in $services; do
        wait_healthy "$svc"
    done

    log "=== Deployment completed successfully ==="
}

main "$@"

Ví dụ thực hành: Script tự động restart container khi crash

Đây là script hoàn chỉnh kết hợp tất cả kỹ thuật trên, được thiết kế chạy qua cron để giữ container luôn hoạt động:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#!/usr/bin/env bash
# ==============================================================================
# Script : docker-watchdog.sh
# Mục đích: Giám sát container và tự động restart khi crash
# Chạy qua cron: */2 * * * * /opt/scripts/docker-watchdog.sh
# ==============================================================================

set -euo pipefail
IFS=$'\n\t'

readonly WATCHED_CONTAINERS="${WATCHED_CONTAINERS:-nginx,api,worker}"
readonly MAX_RESTART="${MAX_RESTART:-3}"
readonly RESTART_WINDOW="${RESTART_WINDOW:-300}"
readonly LOG_FILE="/var/log/docker-watchdog.log"
readonly STATE_DIR="/var/run/docker-watchdog"

mkdir -p "$STATE_DIR"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" 2>/dev/null
}

# Đếm số lần restart trong khoảng thời gian
restart_count() {
    local container="$1"
    local state_file="${STATE_DIR}/${container}.restarts"
    local count=0

    if [[ -f "$state_file" ]]; then
        while IFS= read -r timestamp; do
            if [[ $(date +%s) -lt $((timestamp + RESTART_WINDOW)) ]]; then
                count=$((count + 1))
            fi
        done < "$state_file"

        # Dọn timestamp cũ
        awk -v cutoff=$(($(date +%s) - RESTART_WINDOW)) '$1 >= cutoff' "$state_file" > "${state_file}.tmp"
        mv "${state_file}.tmp" "$state_file"
    fi

    echo "$count"
}

record_restart() {
    local container="$1"
    echo "$(date +%s)" >> "${STATE_DIR}/${container}.restarts"
}

# Kiểm tra container có đang chạy không
is_running() {
    docker ps --format '{{.Names}}' | grep -q "^$1$"
}

# Kiểm tra container có tồn tại không
container_exists() {
    docker ps -a --format '{{.Names}}' | grep -q "^$1$"
}

restart_container() {
    local container="$1"
    local count
    count=$(restart_count "$container")

    if [[ "$count" -ge "$MAX_RESTART" ]]; then
        log "[CRIT] $container restarted $count times in ${RESTART_WINDOW}s — manual intervention required"
        return 1
    fi

    log "[WARN] $container is not running — restarting..."
    docker restart "$container" 2>/dev/null
    record_restart "$container"

    sleep 5
    if is_running "$container"; then
        log "[INFO] $container restarted successfully"
    else
        log "[ERROR] $container failed to restart"
        return 1
    fi
}

check_container() {
    local container="$1"

    if ! container_exists "$container"; then
        log "[WARN] $container does not exist — skipping"
        return 0
    fi

    if is_running "$container"; then
        # Kiểm tra health status nếu có
        local health
        health=$(docker inspect --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' "$container" 2>/dev/null)

        if [[ "$health" == "unhealthy" ]]; then
            log "[WARN] $container is unhealthy"
            restart_container "$container"
        fi
    else
        restart_container "$container"
    fi
}

main() {
    IFS=',' read -ra CONTAINERS <<< "$WATCHED_CONTAINERS"

    for container in "${CONTAINERS[@]}"; do
        container=$(echo "$container" | xargs)
        check_container "$container"
    done
}

main "$@"

Cấu hình cron

1
2
3
# Chạy mỗi 2 phút
crontab -e
*/2 * * * * /opt/scripts/docker-watchdog.sh >> /var/log/docker-watchdog-cron.log 2>&1

Kết quả mẫu

1
2
3
4
[2026-08-31 10:00:01] [INFO] nginx is healthy
[2026-08-31 10:00:01] [WARN] api is not running — restarting...
[2026-08-31 10:00:06] [INFO] api restarted successfully
[2026-08-31 10:00:06] [INFO] worker is healthy

Ghi chú triển khai

  • Docker socket permission: Script Bash cần truy cập Docker daemon qua socket /var/run/docker.sock. Đảm bảo user chạy script thuộc nhóm docker hoặc có quyền sudo.
  • BatchMode SSH: Khi chạy script Docker qua SSH (từ bài trước), sử dụng ssh -o BatchMode=yes để tránh prompt xác nhận host key.
  • Container naming convention: Luôn dùng --name khi chạy container để dễ quản lý. Tránh dùng generated ID vì khó nhớ và không ổn định.
  • Resource cleanup: Script watchdog chỉ restart container — không tự động xóa. Kết hợp với docker system prune chạy định kỳ để giải phóng disk space.
  • Logging aggregation: Trong môi trường production, container logs nên được ship đến centralized logging system (ELK, Loki) thay vì chỉ ghi local file.

Lời kết

Kết hợp Bash và Docker tạo ra một công cụ quản lý container mạnh mẽ, linh hoạt và nhẹ nhàng hơn bất kỳ orchestration tool phức tạp nào. Từ việc liệt kê container, kiểm tra health status, dọn dẹp resource đến viết script watchdog tự động restart khi crash — tất cả đều nằm trong tay bạn chỉ với Bash và Docker CLI.

bài tiếp theo, chúng ta sẽ khám phá cách monitoring với Bash: thu thập metric hệ thống, gửi báo cáo qua email/webhook, và tạo dashboard báo cáo hàng ngày.

THẢO LUẬN & BÌNH LUẬN