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
stopvskillvsrm - 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) để:- Tạo các namespaces (PID, NET, MNT, UTS, IPC, USER)
- Áp dụng cgroups (CPU, memory limits)
- Mount filesystem (overlay layers)
- 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 + RedisBug 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 pausevs 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 pausecho 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:
- Docker gửi SIGTERM đến PID 1 trong container
- PID 1 có 10 giây (mặc định) để cleanup gracefully (đóng connections, flush data, v.v.)
- Sau 10s nếu chưa exit → Docker gửi SIGKILL (force kill)
- Tất cả processes trong container bị kill
- Namespaces bị destroy, cgroups bị remove
- 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 startlại - Logs vẫn còn →
docker logsvẫn hoạt động - Filesystem changes vẫn còn → có thể
docker cpdata 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 stdout và stderr 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 downrồidocker-compose uplạ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 stopvàdocker 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 --rmdù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.