Container 1 — Container hoạt động thế nào

13 thg 7, 2026 3 lượt xem
#docker
#devops
#container
#oci
#namespaces
#cgroups

"Chạy máy tôi thì được mà!"

Có một câu nói huyền thoại trong ngành phần mềm, thốt ra bởi lập trình viên khi code chạy ngon trên laptop của họ nhưng lăn ra chết trên server: "Works on my machine" — "chạy trên máy tôi thì được".

Vấn đề nằm ở chỗ: một ứng dụng không sống một mình. Nó phụ thuộc vào phiên bản ngôn ngữ (Python 3.11 chứ không phải 3.9), vào các thư viện hệ thống (libssl phiên bản nào), vào biến môi trường, vào cấu hình, vào cả kiểu chữ locale của hệ điều hành. Máy của bạn có một tổ hợp các thứ đó; server sản xuất có một tổ hợp khác; máy của đồng nghiệp lại khác nữa. Chỉ cần một mắt xích lệch pha là ứng dụng gãy.

Trước đây người ta giải quyết bằng tài liệu ("cài đúng các bước này…"), bằng script cài đặt, hoặc bằng cách phát cho nhau nguyên một máy ảo (virtual machine) đã cài sẵn mọi thứ. Cách cuối cùng đúng về nguyên lý — đóng gói cả môi trường lại — nhưng quá nặng nề: một máy ảo cõng theo cả một hệ điều hành đầy đủ, tốn hàng GB và vài phút để khởi động.

Container ra đời để giải quyết đúng bài toán này một cách nhẹ nhàng: đóng gói ứng dụng cùng toàn bộ phụ thuộc của nó vào một đơn vị chuẩn hoá, chạy giống hệt nhau ở mọi nơi — laptop của bạn, máy CI, hay server sản xuất. Bài này mổ xẻ cơ chế thật sự bên dưới để bạn không còn coi container là hộp đen.


Container vs Máy ảo (VM)

Đây là điểm hay bị hiểu sai nhất, nên ta làm rõ ngay từ đầu: container không phải là một máy ảo nhỏ.

Máy ảo hoạt động thế nào

Máy ảo dựa trên ảo hoá phần cứng. Một phần mềm gọi là hypervisor (VMware, KVM, Hyper-V…) giả lập ra phần cứng ảo — CPU ảo, RAM ảo, ổ đĩa ảo, card mạng ảo. Trên phần cứng ảo đó, bạn cài một hệ điều hành khách (guest OS) đầy đủ, có kernel riêng của nó. Ứng dụng chạy bên trong guest OS đó.

Hệ quả:

  • Mỗi VM cõng theo một kernel + một OS hoàn chỉnh → tốn hàng trăm MB đến vài GB chỉ cho phần OS, chưa tính ứng dụng.
  • Khởi động VM tức là boot cả một hệ điều hành → mất từ vài chục giây đến vài phút.
  • Cách ly (isolation) rất mạnh: hai VM gần như không thể "nhìn thấy" nhau vì chúng có kernel riêng biệt.

Container hoạt động thế nào

Container không giả lập phần cứng và không có OS riêng. Nó chia sẻ kernel của máy chủ (host). Một container thực chất chỉ là một (hoặc vài) tiến trình bình thường đang chạy trên host, nhưng được kernel "che mắt" lại để tưởng rằng mình đang chạy một mình trong một máy riêng.

Nói cách khác: container không phải máy ảo, nó là tiến trình cô lập. Sự cô lập đó do chính kernel Linux dựng lên bằng các cơ chế ta sẽ nói ngay dưới.

Hệ quả:

  • Không có guest OS, không boot kernel → container khởi động trong mili-giây đến vài giây.
  • Nhẹ: một container chỉ chứa ứng dụng + phụ thuộc của nó, thường vài chục MB.
  • Trên một host có thể chạy hàng trăm container, so với chỉ vài chục VM.
  • Đánh đổi: vì dùng chung kernel host, cách ly kém tuyệt đối hơn VM. Container Linux cần kernel Linux; bạn không chạy container Windows "thuần" trên kernel Linux được.

Sơ đồ trên cho thấy khác biệt cốt lõi: bên VM mỗi ứng dụng cõng nguyên một OS + kernel riêng; bên container tất cả dùng chung một kernel host, mỗi container chỉ đóng gói phần ứng dụng và thư viện của mình.

Đây không phải chuyện "cái nào tốt hơn". VM và container giải quyết các mức độ cô lập khác nhau. Thực tế phổ biến là chạy container bên trong VM: nhà cung cấp cloud cho bạn VM để cách ly cứng giữa các khách hàng, còn bạn dùng container để đóng gói và điều phối ứng dụng bên trong VM đó.


Cơ chế Linux đằng sau container

Container không phải một tính năng "nguyên khối" nào cả. Nó là sự lắp ghép của vài cơ chế có sẵn trong kernel Linux. Hiểu ba mảnh ghép này thì bạn hiểu container.

1. Namespaces — "che mắt" về mặt tài nguyên

Namespace là cơ chế kernel Linux dùng để cô lập tầm nhìn của một tiến trình về hệ thống. Mỗi namespace bao một loại tài nguyên và làm cho tiến trình bên trong chỉ thấy phần của mình, tưởng như cả hệ thống chỉ có mình nó. Các loại chính:

  • PID namespace — cô lập cây tiến trình. Bên trong container, tiến trình chính có PID 1 như thể nó là init của cả hệ thống, và nó không thấy các tiến trình khác của host.
  • NET namespace — cô lập mạng. Mỗi container có stack mạng riêng: interface, địa chỉ IP, bảng định tuyến, cổng riêng. Hai container có thể cùng "nghe" cổng 8080 mà không đụng nhau.
  • MNT namespace — cô lập hệ thống tập tin đã mount. Container thấy một cây thư mục / riêng, không thấy đĩa của host.
  • UTS namespace — cô lập hostname và tên miền. Container có thể có hostname riêng.
  • IPC namespace — cô lập giao tiếp liên tiến trình (shared memory, message queue).
  • USER namespace — ánh xạ user/group. Cho phép root (UID 0) bên trong container được ánh xạ thành một user không đặc quyền ngoài host — nền tảng cho container "rootless" an toàn hơn.

Khi Docker khởi chạy một container, nó tạo cho tiến trình đó một bộ namespace mới. Đó là lý do tại sao bên trong container ps aux chỉ thấy vài tiến trình của mình, còn ngoài host thì ps thấy đúng tiến trình đó lẫn với mọi thứ khác — cùng một tiến trình, hai tầm nhìn khác nhau.

2. Cgroups — giới hạn tài nguyên

Namespace lo phần thấy gì, còn cgroups (control groups) lo phần dùng được bao nhiêu. Cgroups cho phép kernel giới hạn và tính toán lượng tài nguyên một nhóm tiến trình được tiêu thụ:

  • Giới hạn CPU (ví dụ container này chỉ được dùng tối đa 0.5 lõi).
  • Giới hạn RAM (ví dụ tối đa 512MB; vượt thì tiến trình bị kill — lỗi OOMKilled quen thuộc).
  • Giới hạn băng thông đĩa, số tiến trình, v.v.

Nếu không có cgroups, một container "tham lam" có thể ngốn hết RAM/CPU và làm chết mọi container khác trên cùng host. Cgroups chính là thứ khiến việc "nhồi hàng trăm container lên một máy" trở nên an toàn và dự đoán được.

3. OverlayFS — hệ thống tập tin xếp lớp

Mảnh ghép cuối là overlay filesystem. Đây là cách container dựng ra cây thư mục / của nó bằng cách xếp chồng nhiều lớp lên nhau: các lớp bên dưới chỉ đọc (đến từ image), và một lớp mỏng ghi được ở trên cùng dành riêng cho container. Ta nói kỹ ở phần image ngay sau đây.

Tóm lại: container = tiến trình bình thường + namespaces (cô lập tầm nhìn) + cgroups (giới hạn tài nguyên) + overlayfs (hệ tập tin xếp lớp). Không có phép màu, không có VM ẩn — chỉ là các tính năng của kernel Linux được lắp ghép khéo léo.


Image & các lớp (layers)

Ta đã nhắc "image" nhiều lần. Giờ làm rõ khác biệt imagecontainer — hai khái niệm dễ lẫn nhất.

  • Image là một khuôn mẫu chỉ đọc: một gói đóng băng gồm hệ tập tin (ứng dụng + thư viện + runtime) và metadata (lệnh khởi động, biến môi trường…). Image không chạy; nó nằm im như một tập tin cài đặt.
  • Container là một thể hiện đang chạy của một image. Từ một image bạn tạo ra nhiều container, giống như từ một class bạn tạo ra nhiều object.

Image được xếp thành lớp

Một image không phải một khối liền. Nó gồm nhiều lớp (layers) chỉ đọc xếp chồng lên nhau. Mỗi lớp là kết quả của một bước xây dựng: lớp OS nền, lớp cài Python, lớp cài thư viện, lớp copy code ứng dụng…

Cấu trúc lớp này mang lại hai lợi ích lớn:

  1. Chia sẻ lớp (layer sharing). Nếu 10 image cùng dựa trên lớp nền ubuntu:22.04, lớp đó chỉ lưu một lần trên đĩa và được dùng chung. Tải image mới về, Docker chỉ tải các lớp còn thiếu.
  2. Cache khi build & khi kéo về. Khi build lại, các lớp không đổi được tái sử dụng thay vì làm lại — build nhanh hơn nhiều. Ta khai thác kỹ điều này ở bài Dockerfile.

Container = image + một lớp ghi mỏng

Khi bạn khởi chạy container, Docker lấy các lớp chỉ đọc của image, xếp chúng lại bằng overlayfs, rồi thêm một lớp ghi được (writable layer) mỏng lên trên cùng. Mọi thay đổi container tạo ra khi chạy (ghi file, tạo log…) chỉ nằm trong lớp mỏng này. Các lớp image bên dưới không bao giờ bị sửa.

Điều này giải thích hai điều thực tế:

  • Nhiều container từ cùng một image rất tiết kiệm đĩa: chúng dùng chung các lớp chỉ đọc, mỗi container chỉ tốn thêm một lớp ghi mỏng của riêng nó.
  • Dữ liệu trong container là tạm thời (ephemeral): xoá container thì lớp ghi cũng mất. Muốn giữ dữ liệu lâu dài phải dùng volume (nói ở bài Compose & mạng).

OCI — chuẩn hoá để không khoá vào Docker

Ban đầu định dạng image là của riêng Docker. Về sau ngành lập ra OCI (Open Container Initiative) để chuẩn hoá định dạng imageruntime. Nhờ OCI, một image build ra không bị khoá cứng vào Docker: bạn có thể build bằng Docker rồi chạy bằng công cụ khác (Podman, containerd) miễn là cùng tuân theo OCI. Đây là lý do hệ sinh thái container ngày nay tương thích chéo được với nhau.

Registry — nơi cất và chia sẻ image

Registry là kho chứa image trên mạng. Bạn push image lên registry để chia sẻ, và pull về khi cần chạy. Registry công khai mặc định là Docker Hub; trong doanh nghiệp thường dùng registry riêng (Harbor, AWS ECR, GitLab Registry…). Một image được định danh dạng registry/repository:tag, ví dụ python:3.11-slim hay myregistry.company.com/data/airflow:2.9.


Kiến trúc Docker

Từ "Docker" hay bị dùng lẫn lộn. Thực ra Docker gồm nhiều thành phần phối hợp, và hiểu rõ chúng giúp bạn không bối rối khi nghe tin "Kubernetes bỏ Docker".

  • Docker CLI (docker) — công cụ dòng lệnh bạn gõ. Nó không tự chạy container; nó chỉ gửi yêu cầu qua REST API tới daemon.
  • Docker daemon (dockerd) — tiến trình nền làm phần việc nặng: build image, quản lý image/network/volume, và điều phối việc chạy container. Đây là "bộ não" mà CLI ra lệnh.
  • containerd — runtime cấp cao mà dockerd uỷ thác việc quản lý vòng đời container (kéo image, tạo/chạy/dừng).
  • runc — runtime cấp thấp, là thứ thực sự gọi vào kernel để tạo namespaces + cgroups và sinh ra tiến trình container. runc tuân theo chuẩn runtime OCI.
  • Registry — kho image ở ngoài, như đã nói.

Chuỗi uỷ thác đi từ trên xuống: docker (CLI) → dockerdcontainerdrunc → tiến trình container.

Còn Podman, containerd, và chuyện "K8s bỏ Docker"?

  • Podman là công cụ tương thích lệnh với Docker nhưng không có daemon và hỗ trợ chạy rootless tốt — một lựa chọn thay thế phổ biến, đặc biệt trên hệ Red Hat.
  • containerd có thể chạy độc lập không cần dockerd, nên nhiều nền tảng dùng thẳng nó.
  • Về tin "Kubernetes bỏ Docker": Kubernetes từng dùng một lớp cầu nối gọi là dockershim để nói chuyện với Docker. Từ Kubernetes 1.24, dockershim bị loại bỏ, và K8s giao tiếp trực tiếp với runtime tuân theo chuẩn CRI như containerd (vốn chính là phần lõi bên trong Docker). Điều quan trọng cần nắm: image bạn build bằng Docker vẫn chạy tốt vì chúng theo chuẩn OCI — chỉ có "người điều phối" phía dưới đổi từ dockerd sang containerd, còn định dạng đóng gói không đổi.

Vòng đời một container

Một container đi qua các trạng thái khá đơn giản. Dưới đây là dòng chảy điển hình cùng các lệnh Docker cơ bản.

# (minh hoạ) Kéo image từ registry về máy
docker pull python:3.11-slim

# (minh hoạ) Tạo và chạy container luôn (create + start)
docker run -d --name web -p 8080:80 nginx

# (minh hoạ) Liệt kê container đang chạy
docker ps

# (minh hoạ) Liệt kê tất cả container, kể cả đã dừng
docker ps -a

# (minh hoạ) Xem log của container
docker logs web

# (minh hoạ) Chui vào bên trong container đang chạy để debug
docker exec -it web bash

# (minh hoạ) Dừng rồi xoá container
docker stop web
docker rm web

Lưu ý về môi trường sandbox của Knowledge Base này: nó chạy trên PostgreSQL, không có Docker daemon, nên các lệnh docker/kubectl trong bài chỉ để minh hoạ, không chạy được ở đây. Bạn hãy chạy chúng trên máy có cài Docker.

Các giai đoạn trong vòng đời:

  1. pull — kéo image về máy (bỏ qua nếu đã có sẵn).
  2. create — dựng container từ image (tạo lớp ghi + cấu hình) nhưng chưa chạy.
  3. start — khởi động tiến trình chính bên trong container. Lệnh docker run gộp create + start cho tiện.
  4. stop — gửi tín hiệu dừng lịch sự (SIGTERM, rồi SIGKILL nếu quá hạn). Container chuyển sang trạng thái exited nhưng lớp ghi vẫn còn.
  5. rm — xoá hẳn container và lớp ghi của nó.

Các lệnh tra cứu/vận hành thường dùng: docker ps (đang chạy), docker ps -a (tất cả), docker logs (xem log), docker exec (chạy lệnh bên trong để debug).


Use case thực tế

Với người làm Data Engineering, container không phải chuyện xa lạ mà là công cụ hằng ngày:

  • Đóng gói job Spark nhất quán. Thay vì cầu mong mọi node trong cụm cài đúng cùng phiên bản Spark, Java và thư viện, bạn đóng tất cả vào một image. Job chạy trên máy dev, trên CI, và trên cụm sản xuất bằng cùng một image → hết cảnh "chạy máy tôi thì được".
  • Chạy Airflow tái lập được. Airflow có nhiều thành phần (webserver, scheduler, worker) và phụ thuộc phức tạp. Đóng chúng thành image giúp toàn đội chạy đúng cùng một môi trường, và nâng cấp phiên bản chỉ là đổi tag image.
  • Dựng môi trường thử nghiệm chớp nhoáng. Cần một PostgreSQL để test? docker run một container, xong việc thì xoá — không làm bẩn máy, không xung đột phiên bản với dự án khác.
  • Nền cho điều phối ở quy mô lớn. Khi số lượng service tăng, chính các image container này là đơn vị mà Kubernetes đem đi triển khai, tự phục hồi và mở rộng trên cả cụm nhiều máy.

Điểm chung của mọi use case: container biến "môi trường chạy" từ một thứ mong manh, khó tái lập thành một artifact đóng gói được, phiên bản hoá được, chia sẻ được — giống như cách chúng ta vốn quen quản lý mã nguồn.


Ghi nhớ

  • Container không phải VM nhỏ. VM ảo hoá phần cứng và cõng theo cả một OS + kernel riêng (nặng, khởi động chậm); container chia sẻ kernel host, chỉ là tiến trình cô lập (nhẹ, khởi động nhanh).
  • Ba mảnh ghép kernel Linux làm nên container: namespaces (cô lập tầm nhìn: pid/net/mnt/uts/ipc/user), cgroups (giới hạn CPU/RAM), overlayfs (hệ tập tin xếp lớp).
  • Image ≠ container. Image là khuôn chỉ đọc, gồm nhiều lớp xếp chồng dùng chung được; container = image + một lớp ghi mỏng. Dữ liệu trong lớp ghi là tạm thời.
  • OCI chuẩn hoá định dạng image & runtime nên image không bị khoá vào Docker; registry là nơi push/pull chia sẻ image.
  • Kiến trúc Docker: docker CLI → dockerdcontainerdrunc → tiến trình container; ngoài ra có Podman, containerd độc lập. K8s từ 1.24 bỏ dockershim và dùng thẳng containerd — nhưng image OCI của bạn vẫn chạy nguyên vẹn.
  • Vòng đời: pull → create → start → stop → rm; vận hành bằng ps, logs, exec.

Đi tiếp

Tham khảo chính thức: docs.docker.com và tài liệu Open Container Initiative (OCI).

Bài viết liên quan

Continuous Integration/Delivery/Deployment, cấu trúc pipeline, ví dụ GitHub Actions và chiến lược release.

13 thg 7, 2026 4

Container vs máy ảo, image/layer, Dockerfile, volume, network, Docker Compose và best practices.

13 thg 7, 2026 3

Vì sao cần orchestration; Pod, Deployment, Service, Ingress; scaling, self-healing và cấu hình.

13 thg 7, 2026 3

Bản chất DevOps: phá bỏ rào cản Dev–Ops, vòng lặp vô tận, CALMS, và vì sao tự động hoá.

13 thg 7, 2026 3