Docker Deev Dive #4:Container Lifecycle — Vòng đời container

19 phút đọc

Vấn đề gốc — Tại sao cần hiểu Container Lifecycle?

Bạn đã biết container chỉ là Linux process bị cô lập. Nhưng process có vòng đời — nó được tạo ra, chạy, dừng, và kết thúc. Hiểu vòng đời container giúp bạn:

  • Debug tại sao container không start được
  • Hiểu tại sao dữ liệu mất khi xóa container
  • Biết khi nào dùng stop vs kill vs rm
  • Tránh resource leak (container zombie chiếm tài nguyên)

Câu hỏi cốt lõi: Container đi qua những trạng thái nào từ lúc sinh ra đến lúc bị xóa? Mỗi trạng thái tương ứng với chuyện gì xảy ra ở tầng kernel?


First Principle — Container = Process + Writable Layer

┌──────────────────────────────────┐
│       Writable Layer             │  ← Container tạo/sửa file ở đây
│   (sinh ra khi create,           │
│    mất khi rm)                   │
├──────────────────────────────────┤
│       Image Layers (read-only)   │  ← Không bao giờ thay đổi
│   nginx:alpine base              │
└──────────────────────────────────┘

Khi bạn docker exec vào container và tạo file, file đó nằm ở writable layer. Xóa container = xóa writable layer = mất file.


Sơ đồ vòng đời Container

                docker create
     ┌──────────────────────────┐
     │                          ▼
┌─────────┐    docker start   ┌──────────┐
│ CREATED  │─────────────────▶│ RUNNING  │
└─────────┘                   └────┬─────┘
     ▲                             │
     │                    ┌────────┼──────────┐
     │                    │        │          │
     │            docker  │  docker│   docker │
     │            pause   │  stop  │   kill   │
     │                    ▼        ▼          │
     │              ┌────────┐  ┌───────┐    │
     │              │ PAUSED │  │EXITED │    │
     │              └───┬────┘  └───┬───┘    │
     │                  │           │         │
     │           docker │    docker │         │
     │           unpause│    start  │         │
     │                  │           │         │
     │                  ▼           ▼         │
     │              ┌──────────────────┐      │
     │              │    RUNNING       │◀─────┘
     │              └──────────────────┘
     │                       │
     │                docker rm (khi exited)
     │                       │
     │                       ▼
     │              ┌──────────────────┐
     └──────────────│    REMOVED       │
                    └──────────────────┘

5 trạng thái của Container

1. CREATED — Đã tạo, chưa chạy

docker create --name my-nginx nginx:alpine

Chuyện gì xảy ra ở tầng kernel:

  • Docker daemon tạo writable layer (Union FS) trên image
  • Chuẩn bị config (environment variables, network, mounts)
  • Chưa tạo namespaces, chưa chạy process nào
  • Container tồn tại trên disk nhưng không tiêu CPU/RAM

Khi nào dùng create thay vì run?

  • Chuẩn bị container trước, start sau (CI/CD pipeline)
  • Muốn inspect hoặc modify filesystem trước khi chạy
  • Batch provisioning — tạo nhiều container, start đồng loạt
# Verify: container ở trạng thái Created
docker ps -a --filter "name=my-nginx"
# STATUS: Created

2. RUNNING — Đang chạy

docker start my-nginx
# hoặc tạo + chạy luôn:
docker run -d --name my-nginx nginx:alpine

Chuyện gì xảy ra ở tầng kernel:

  • Docker gọi container runtime (runc) để:
    1. Tạo các namespaces (PID, NET, MNT, UTS, IPC, USER)
    2. Áp dụng cgroups (CPU, memory limits)
    3. Mount filesystem (overlay layers)
    4. Chạy ENTRYPOINT/CMD process — đây là PID 1 trong container
  • Process PID 1 bắt đầu chạy → container ở trạng thái Running
# Container đang chạy
docker ps
# CONTAINER ID  IMAGE         STATUS        NAMES
# a1b2c3d4e5f6  nginx:alpine  Up 2 minutes  my-nginx

3. PAUSED — Đóng băng tạm thời

  • Giải thích thêm

    Câu hỏi rất hay! Race condition không chỉ xảy ra ở database — nó có thể xảy ra ở bất kỳ đâu có nhiều thành phần chạy đồng thời và chia sẻ tài nguyên. Để giải thích use case "freeze container A, test container B":


    Race Condition trong ngữ cảnh Container

    Race condition là gì (nhắc lại nhanh)

    Hai hoặc nhiều thành phần cùng truy cập/thay đổi một tài nguyên chung, và kết quả phụ thuộc vào thứ tự thực thi — thứ tự đó không thể đoán trước.

    Bạn quen race condition ở database (2 request cùng update 1 row). Nhưng trong hệ thống microservices/container, race condition xảy ra ở nhiều tầng hơn:

    Tầng Ví dụ race condition
    Database 2 request cùng update balance
    File system 2 container cùng ghi vào shared volume
    Network/API Container A gọi API → Container B chưa sẵn sàng
    Message Queue 2 worker cùng consume 1 message
    Shared Cache Container A ghi Redis key, Container B đọc stale data

    Scenario cụ thể: "Freeze container A, test container B"

    Hãy tưởng tượng hệ thống Laravel của bạn:

    Container A: php-fpm (xử lý request đặt phòng)
    Container B: queue-worker (xử lý job tính tiền)
    Shared: MySQL + Redis
    

    Bug nghi ngờ: Khi user đặt phòng, đôi khi tiền bị tính 2 lần. Bạn nghi race condition giữa container A (tạo booking) và container B (queue worker xử lý payment job).

    Vấn đề khi debug bình thường: Cả 2 container chạy đồng thời, mọi thứ xảy ra trong mili-giây — bạn không thể quan sát trạng thái trung gian.

    Giải pháp: dùng docker pause để "đóng băng thời gian":

    # Bước 1: Freeze container A (php-fpm) — nó đang giữa chừng xử lý request
    docker pause laravel-app
    
    # Bước 2: Kiểm tra trạng thái hiện tại
    # → Database: booking đã tạo chưa? Payment job đã dispatch chưa?
    # → Redis: lock key có tồn tại không?
    docker exec mysql mysql -e "SELECT * FROM bookings WHERE id = 123"
    docker exec redis redis-cli GET "payment:lock:123"
    
    # Bước 3: Chạy container B (queue worker) riêng
    # → Xem worker xử lý thế nào khi app container đang bị freeze giữa chừng
    docker exec laravel-worker php artisan queue:work --once
    
    # Bước 4: Quan sát kết quả
    # → Worker tính tiền 1 lần hay 2 lần? Có check lock không?
    
    # Bước 5: Unpause container A — nó tiếp tục từ đúng chỗ bị dừng
    docker unpause laravel-app
    # → App tiếp tục xử lý → có dispatch payment job lần 2 không?
    

    Tại sao cách này hiệu quả?

    Bình thường, mọi thứ xảy ra quá nhanh để quan sát:

    Không pause (bình thường):
      A: tạo booking ──→ dispatch job ──→ response    (5ms)
      B:                    consume job ──→ charge     (3ms)
    
      ← Tất cả xảy ra trong 8ms, không kịp xem gì →
    

    Khi dùng docker pause:

    Có pause:
      A: tạo booking ──→ ❄️ FROZEN ❄️
                              │
      Bạn:  kiểm tra DB ←────┘
             kiểm tra Redis
             chạy worker thủ công
             quan sát kết quả
                              │
      A: ──→ dispatch job ←───┘ (unpause)
    
      ← Bạn có thời gian vô hạn để kiểm tra trạng thái trung gian →
    

    Tóm lại

    Câu hỏi Trả lời
    Race condition chỉ ở database? ❌ Xảy ra ở mọi nơi có shared resource + concurrency
    Tại sao freeze container giúp debug? Dừng 1 thành phần để quan sát trạng thái trung gian mà bình thường xảy ra quá nhanh
    docker pause vs breakpoint? Pause freeze toàn bộ container (tất cả processes), breakpoint chỉ dừng 1 thread trong 1 process

    Nói cách khác, docker pause cho bạn khả năng "slow motion" hệ thống phân tán — thứ mà debugger truyền thống không làm được vì nó chỉ debug được 1 process.

docker pause my-nginx

Chuyện gì xảy ra ở tầng kernel:

  • Docker sử dụng cgroup freezer — một cgroup subsystem đặc biệt
  • Tất cả processes trong container bị SIGSTOP (frozen)
  • Process vẫn ở trong memory, không bị kill, nhưng không được CPU schedule
  • Giống như "đóng băng thời gian" — process không biết nó bị pause

Khi nào dùng pause?

  • Debug race condition: freeze container A, test container B
  • Tạm dừng container đang dùng nhiều CPU mà không muốn kill
  • Hot-maintenance trên host
# Unpause để tiếp tục
docker unpause my-nginx
# Container quay lại Running, process tiếp tục chạy như không có gì xảy ra

4. EXITED (STOPPED) — Đã dừng

Container dừng qua 2 cách:

Cách 1: Graceful stop (docker stop)

docker stop my-nginx

Cơ chế bên trong:

  1. Docker gửi SIGTERM đến PID 1 trong container
  2. PID 1 có 10 giây (mặc định) để cleanup gracefully (đóng connections, flush data, v.v.)
  3. Sau 10s nếu chưa exit → Docker gửi SIGKILL (force kill)
  4. Tất cả processes trong container bị kill
  5. Namespaces bị destroy, cgroups bị remove
  6. Writable layer vẫn còn trên disk
# Tùy chỉnh timeout (ví dụ 30 giây)
docker stop -t 30 my-nginx

Cách 2: Force kill (docker kill)

docker kill my-nginx

Cơ chế bên trong:

  • Gửi SIGKILL ngay lập tức — không có grace period
  • Process bị terminate ngay, không có cơ hội cleanup
  • Chỉ dùng khi container không phản hồi docker stop

PID 1 tự exit

Nếu process chính tự kết thúc (ví dụ script chạy xong), container cũng chuyển sang Exited với exit code tương ứng.

# Exit code 0 = thành công
docker run ubuntu echo "hello"
# → Container chạy echo, in "hello", exit code 0

# Exit code khác 0 = lỗi
docker run ubuntu cat /file-khong-ton-tai
# → cat thất bại, exit code 1

# Xem exit code
docker inspect --format='.State.ExitCode' <container-name>

Trạng thái Exited quan trọng vì:

  • Writable layer vẫn còn → có thể docker start lại
  • Logs vẫn còndocker logs vẫn hoạt động
  • Filesystem changes vẫn còn → có thể docker cp data ra
  • Nhưng container đang chiếm disk space → cần dọn dẹp

5. REMOVED — Bị xóa hoàn toàn

docker rm my-nginx

Chuyện gì xảy ra:

  • Writable layer bị xóa khỏi disk
  • Metadata (config, logs, network config) bị xóa
  • Container không thể khôi phục
  • Image layers không bị ảnh hưởng (vẫn dùng để tạo container khác)
# Không thể rm container đang Running (phải stop trước)
docker rm my-nginx
# Error: cannot remove running container

# Force remove (stop + rm trong 1 lệnh)
docker rm -f my-nginx

# Xóa tất cả container đã dừng
docker container prune

Bảng tổng hợp: Trạng thái vs Tài nguyên

Trạng thái CPU/RAM Disk (Writable Layer) Network Logs
Created ❌ Không dùng ✅ Đã tạo ❌ Chưa kết nối ❌ Chưa có
Running ✅ Đang dùng ✅ Đang ghi ✅ Kết nối ✅ Đang ghi
Paused ❌ Frozen (vẫn giữ RAM) ✅ Giữ nguyên ❌ Frozen ❌ Frozen
Exited ❌ Không dùng ✅ Vẫn còn ❌ Ngắt ✅ Vẫn đọc được
Removed ❌ Bị xóa ❌ Bị xóa

Các lệnh quan trọng trong Lifecycle

docker run — Tạo + Start trong 1 lệnh

docker run = docker create + docker start. Đây là lệnh bạn dùng nhiều nhất.

# Cú pháp đầy đủ
docker run [OPTIONS] IMAGE [COMMAND] [ARGS...]

# Chạy foreground (thấy output, Ctrl+C để dừng)
docker run --name web nginx:alpine

# Chạy background (detached mode)
docker run -d --name web nginx:alpine

# Chạy interactive + tty (vào shell)
docker run -it --name test ubuntu:22.04 /bin/bash

# Tự động xóa khi exit (--rm)
docker run --rm ubuntu:22.04 echo "hello"
# → Chạy echo, exit, container tự bị rm

docker exec — Chạy lệnh trong container đang Running

# Mở shell bên trong container
docker exec -it my-nginx /bin/sh

# Chạy một lệnh cụ thể
docker exec my-nginx cat /etc/nginx/nginx.conf

# Chạy với user khác
docker exec -u root my-nginx whoami

Bản chất: docker exec tạo thêm một process mới bên trong các namespaces đang tồn tại của container. Process này không phải PID 1 → nếu nó exit, container vẫn chạy.

docker logs — Xem output của container

# Xem tất cả logs
docker logs my-nginx

# Follow logs (real-time, như tail -f)
docker logs -f my-nginx

# Xem 50 dòng cuối
docker logs --tail 50 my-nginx

# Xem logs từ thời điểm cụ thể
docker logs --since 2024-01-01T00:00:00 my-nginx

Bản chất: Docker capture stdoutstderr của PID 1 và lưu vào log driver (mặc định: json-file trên disk). Đây là lý do ứng dụng trong container nên log ra stdout, không ghi file.

docker inspect — Xem toàn bộ metadata

# Xem full JSON metadata
docker inspect my-nginx

# Lấy thông tin cụ thể
docker inspect --format='.State.Status' my-nginx
# → running

docker inspect --format='.NetworkSettings.IPAddress' my-nginx  
# → 172.17.0.2

docker inspect --format='.State.Pid' my-nginx
# → 28374 (PID trên host)

Bản chất: inspect trả về toàn bộ config + runtime state dưới dạng JSON. Rất hữu ích để debug: xem IP, mounts, environment variables, exit code, restart count, v.v.

docker stats — Monitor resource usage real-time

# Xem tất cả container đang chạy
docker stats

# Xem container cụ thể
docker stats my-nginx

# Output:
# CONTAINER  CPU %  MEM USAGE/LIMIT   MEM %  NET I/O      BLOCK I/O
# my-nginx   0.05%  2.5MiB / 7.7GiB   0.03%  1.2kB/648B   0B/0B

Thực hành: Chứng minh Container là Ephemeral

Đây là bài thực hành quan trọng nhất của step này. Mục tiêu: chứng minh bằng thực nghiệm rằng dữ liệu mất khi xóa container.

Bước 1: Tạo container và ghi dữ liệu

# Chạy nginx container
docker run -d --name test-nginx nginx:alpine

# Exec vào bên trong
docker exec -it test-nginx /bin/sh

# Tạo file bên trong container
echo "Hello from inside container!" > /tmp/my-data.txt
cat /tmp/my-data.txt
# → Hello from inside container!

# Tạo thêm file trong webroot
echo "<h1>Custom Page</h1>" > /usr/share/nginx/html/custom.html

exit

Bước 2: Verify file tồn tại

# File vẫn còn (container vẫn Running)
docker exec test-nginx cat /tmp/my-data.txt
# → Hello from inside container!

# Stop container
docker stop test-nginx

# Start lại → file VẪN CÒN (writable layer chưa bị xóa)
docker start test-nginx
docker exec test-nginx cat /tmp/my-data.txt
# → Hello from inside container!  ✅

Bước 3: Xóa container → dữ liệu MẤT

# Xóa container
docker stop test-nginx
docker rm test-nginx

# Chạy container mới từ cùng image
docker run -d --name test-nginx-2 nginx:alpine

# File KHÔNG CÒN!
docker exec test-nginx-2 cat /tmp/my-data.txt
# → cat: can't open '/tmp/my-data.txt': No such file or directory  ❌

# Custom page cũng mất
docker exec test-nginx-2 cat /usr/share/nginx/html/custom.html
# → cat: can't open ...: No such file or directory  ❌

# Cleanup
docker rm -f test-nginx-2

Bước 4: Khám phá writable layer trên host

# Tìm writable layer trên host filesystem
docker run -d --name explore-layer nginx:alpine

# Xem GraphDriver info (nơi writable layer được lưu)
docker inspect --format='.GraphDriver.Data.MergedDir' explore-layer
# → /var/lib/docker/overlay2/<hash>/merged

# Tạo file trong container
docker exec explore-layer sh -c 'echo "test" > /tmp/test.txt'

# Xem UpperDir (writable layer)
docker inspect --format='.GraphDriver.Data.UpperDir' explore-layer
# → /var/lib/docker/overlay2/<hash>/diff

# Trên host (cần root):
sudo ls /var/lib/docker/overlay2/<hash>/diff/tmp/
# → test.txt  (file bạn tạo trong container nằm ở đây!)

# Xóa container → thư mục này bị xóa
docker rm -f explore-layer

Restart Policies — Tự động khởi động lại

Docker hỗ trợ restart policies để tự động restart container khi nó crash hoặc khi Docker daemon khởi động lại.

# Không restart (mặc định)
docker run -d --restart=no nginx

# Luôn restart (kể cả khi exit thành công)
docker run -d --restart=always nginx

# Restart trừ khi bị stop thủ công
docker run -d --restart=unless-stopped nginx

# Restart khi exit code ≠ 0 (lỗi), tối đa 5 lần
docker run -d --restart=on-failure:5 nginx
Policy Restart khi crash? Restart khi Docker restart? Restart khi stop thủ công?
no
always ✅ (sau Docker restart)
unless-stopped ❌ (nếu đã stop trước đó)
on-failure:N ✅ (tối đa N lần)

Trong Laravel project, thường dùng unless-stopped hoặc always cho production containers (nginx, php-fpm, mysql).


Dọn dẹp — Quản lý container lifecycle hiệu quả

# Xem tất cả container (kể cả đã dừng)
docker ps -a

# Xóa tất cả container đã dừng
docker container prune

# Xóa container + volumes không dùng + networks + dangling images
docker system prune

# Xem disk usage
docker system df
# TYPE           TOTAL  ACTIVE  SIZE     RECLAIMABLE
# Images         15     3       2.5GB    1.8GB (72%)
# Containers     20     2       500MB    450MB (90%)
# Local Volumes  10     3       1.2GB    800MB (66%)

Tổng hợp: Lifecycle Flow khi chạy Laravel với Docker

Khi bạn chạy docker-compose up -d cho Laravel project:

1. docker-compose đọc docker-compose.yml

2. Với mỗi service (nginx, php-fpm, mysql):
   ├── Pull image (nếu chưa có)
   ├── Create container (tạo writable layer)
   ├── Create network (bridge cho compose project)
   ├── Mount volumes (nếu có)
   └── Start container (tạo namespaces, cgroups, chạy PID 1)

3. Containers đang Running:
   ├── nginx:80 → php-fpm:9000 (qua bridge network)
   ├── php-fpm → mysql:3306 (qua bridge network)
   └── Volumes: mysql data, laravel code (bind mount)

4. docker-compose down:
   ├── Stop all containers (SIGTERM → SIGKILL)
   ├── Remove containers (xóa writable layers)
   ├── Remove network
   └── Volumes: GIỮ LẠI (trừ khi dùng --volumes flag)

Đây là lý do docker-compose down rồi docker-compose up lại → database vẫn còn data (vì volume giữ lại), nhưng mọi thay đổi bên trong container (ví dụ: file tạo tay trong /tmp) sẽ mất.


Câu hỏi tự kiểm tra

  • Container khác gì process thông thường trên Linux?
  • Giải thích sự khác biệt giữa docker stopdocker kill?
  • Tại sao dữ liệu mất khi xóa container? Dữ liệu đó nằm ở đâu trên host?
  • Container đang ở trạng thái Exited có chiếm CPU/RAM không? Có chiếm disk không?
  • docker run --rm dùng khi nào? Tại sao nó là best practice cho one-off commands?
  • Restart policy nào phù hợp cho production Laravel containers?

Bước tiếp theo

Bạn đã hiểu container là ephemeral — dữ liệu mất khi xóa container. Nhưng database cần persist data. Laravel cần persist storage, logs. Giải pháp: Docker Volume — bước tiếp theo trong lộ trình.


References

Bài viết liên quan

Đang cập nhật...