Docker Image và Container – Đừng nhầm "bản thiết kế" với "thứ đang chạy"
Một trong những nhầm lẫn phổ biến nhất khi mới học Docker là:
Docker Image và Docker Container có phải cùng một thứ không?
Không.
Có thể hiểu rất nhanh:
Nếu Image giống như một "template" bất biến, thì Container là phiên bản thực tế được khởi tạo từ template đó.
Docker Image là gì?
Container Image là một tập hợp dữ liệu được lưu trên storage, chứa những thành phần cần thiết để ứng dụng có thể chạy:
Ví dụ chúng ta có:
httpd:2.4
nginx:latest
python:3.12-slim
myapp:v1.0
Đây đều là Images, chưa phải các ứng dụng đang chạy.
Image thường được tạo tại Build Stage của CI/CD Pipeline, sau đó được đưa lên Container Registry.
Source Code
│
▼
CI/CD Build
│
▼
Docker Image
│
▼
Container Registry
Container là gì?
Muốn ứng dụng thực sự chạy, chúng ta phải tạo Container từ Image.
Docker Image
│
docker run
▼
Container
│
▼
Running Process
Một Image có thể tạo ra nhiều Container:
myapp:v1
│
┌────────┼────────┐
▼ ▼ ▼
Container 1 Container 2 Container 3
Đây chính là một trong những cơ sở để thực hiện Horizontal Scaling – Scale Out mà chúng ta đã nói đến trong 12-Factor App.
Container không phải một Virtual Machine nhỏ
Bên trong Container có một hoặc nhiều Process, nhưng các Process này được cách ly khỏi các Process khác trên Host.
Chúng có góc nhìn giới hạn đối với:
Trên Linux, Docker sử dụng các cơ chế của kernel như Namespaces và cgroups để tạo ra sự cách ly và kiểm soát tài nguyên.
Điểm rất quan trọng:
Ví dụ Host chạy một Linux distribution, Container có thể cung cấp user-space của Debian, Ubuntu hoặc một distribution phù hợp với ứng dụng, nhưng các Linux Container vẫn sử dụng kernel của Host.
Đây là lý do Container nhỏ và khởi động nhanh hơn VM.
docker pull – Lấy Image về máy
Muốn chạy Container, Docker Engine trước tiên phải có Image.
Chúng ta có thể chủ động tải Image:
docker pull httpd
Quá trình sẽ là:
Container Registry
│
docker pull
│
▼
Local Image Cache
Nếu chạy docker run mà Image chưa tồn tại local, Docker thông thường cũng sẽ tự động Pull Image cần thiết từ Registry.
docker run – Từ Image thành Container
Lệnh cơ bản:
docker run httpd
Docker sẽ tạo Container từ Image httpd và khởi động Process mặc định đã được định nghĩa trong Image.
Trong thực tế, chúng ta thường cần thêm một số option.
Ví dụ:
docker run -d --name web01 -p 8080:80 httpd
Hãy bóc tách câu lệnh này. -d – Detached Mode
-d
Container chạy dưới background thay vì chiếm Terminal hiện tại.
--name – Đặt tên Container
--name web01
Container sẽ có tên:
web01
Thay vì phải nhớ một Container ID dài, chúng ta có thể quản lý nó bằng tên.
Tên Container cũng hữu ích trong Docker networking: các Container trên cùng một user-defined network có thể phân giải tên của nhau thông qua DNS tích hợp của Docker.
-p – Publish Port
-p 8080:80
Đây là phần người mới học Docker rất dễ nhầm.
Nó có nghĩa:
Client
│
▼
Host:8080
│
▼
Container:80
│
▼
Apache HTTP Server
Port 8080 của Host được publish/map tới Port 80 của Container.
Sau đó người dùng có thể truy cập:
http://<Docker-Host>:8080
Một lưu ý về thuật ngữ: EXPOSE trong Dockerfile chủ yếu mô tả/document port mà ứng dụng dự kiến lắng nghe; còn docker run -p mới thực hiện việc publish port của Container ra Host.
Điều này liên hệ trực tiếp với yếu tố Port Binding trong 12-Factor App mà chúng ta đã học.
-it – Khi muốn tương tác với Container
Đôi khi chúng ta không muốn chạy Web Server mà muốn vào bên trong môi trường Container để kiểm tra.
Có thể sử dụng:
docker run -it ubuntu bash
Trong đó:
-i → Interactive
-t → Allocate pseudo-TTY
và:
bash
là command được yêu cầu chạy trong Container.
Khi đó thay vì khởi động một service theo mặc định của Image, Docker chạy Bash và cho phép chúng ta tương tác với môi trường bên trong Container.
Lưu ý rằng command chỉ chạy được nếu chương trình đó thực sự tồn tại trong Image.
Tại sao nhiều Container Image không có đầy đủ công cụ Linux?
Bạn vào một Container và thử:
ping
vim
curl
ssh
nhưng phát hiện một số lệnh không tồn tại.
Điều này không nhất thiết là lỗi.
Container Image thường được thiết kế theo nguyên tắc:
Ít package hơn đồng nghĩa với:
Đó là lý do các Image như slim, alpine hoặc các distroless images thường được sử dụng để tối ưu Container cho Production.
Docker Engine đứng ở đâu trong toàn bộ kiến trúc?
Khi bạn gõ:
docker run ...
Docker CLI không tự mình thực hiện tất cả công việc.
Mô hình tổng quát là:
Docker Client
│
Docker API
│
▼
Docker Engine / Daemon
│
├──── Container Images
│
├──── Containers
│
├──── Networks
│
└──── Volumes
Docker Client gửi yêu cầu tới Docker Engine thông qua API, còn Engine chịu trách nhiệm thực hiện các thao tác liên quan đến Image, Container, Network và Volume.
Kết nối lại với CI/CD
Bây giờ chúng ta có thể nhìn Docker dưới góc độ DevSecOps:
Developer
│
▼
Git Commit
│
▼
CI Pipeline
│
├── Static Analysis
├── Unit Test
└── docker build
│
▼
Docker Image
│
▼
Container Registry
│
docker pull
│
▼
docker run
│
▼
Container
Build Stage tạo Image.
Registry lưu và phân phối Image.
Run Stage tạo Container từ Image.
Và từ cùng một Image đã được kiểm thử, chúng ta có thể tạo nhiều Container giống nhau trên Development, Staging hay Production.
Đây chính là điểm Docker kết nối rất đẹp với những nguyên tắc chúng ta đã học trong chuỗi 12-Factor App: Build–Release–Run, Dev/Prod Parity, Port Binding, Stateless Processes, Concurrency và Disposability.
Hiểu rõ Image ≠ Container là bước đầu tiên để hiểu Docker. Bước tiếp theo quan trọng hơn nữa là hiểu Docker tạo Image như thế nào thông qua Dockerfile và hệ thống Layers, bởi đây chính là nơi chúng ta có thể tối ưu tốc độ Build, kích thước Image và cả bảo mật cho CI/CD Pipeline.
Một trong những nhầm lẫn phổ biến nhất khi mới học Docker là:
Docker Image và Docker Container có phải cùng một thứ không?
Không.
Có thể hiểu rất nhanh:
Image là thứ được lưu trữ. Container là một instance đang chạy được tạo ra từ Image.
Nếu Image giống như một "template" bất biến, thì Container là phiên bản thực tế được khởi tạo từ template đó.
Docker Image là gì?
Container Image là một tập hợp dữ liệu được lưu trên storage, chứa những thành phần cần thiết để ứng dụng có thể chạy:
- Application
- Runtime
- Libraries
- Dependencies
- Một phần filesystem cần thiết
Ví dụ chúng ta có:
httpd:2.4
nginx:latest
python:3.12-slim
myapp:v1.0
Đây đều là Images, chưa phải các ứng dụng đang chạy.
Image thường được tạo tại Build Stage của CI/CD Pipeline, sau đó được đưa lên Container Registry.
Source Code
│
▼
CI/CD Build
│
▼
Docker Image
│
▼
Container Registry
Container là gì?
Muốn ứng dụng thực sự chạy, chúng ta phải tạo Container từ Image.
Docker Image
│
docker run
▼
Container
│
▼
Running Process
Một Image có thể tạo ra nhiều Container:
myapp:v1
│
┌────────┼────────┐
▼ ▼ ▼
Container 1 Container 2 Container 3
Đây chính là một trong những cơ sở để thực hiện Horizontal Scaling – Scale Out mà chúng ta đã nói đến trong 12-Factor App.
Container không phải một Virtual Machine nhỏ
Bên trong Container có một hoặc nhiều Process, nhưng các Process này được cách ly khỏi các Process khác trên Host.
Chúng có góc nhìn giới hạn đối với:
- Process
- Filesystem
- Networking
- Resources
Trên Linux, Docker sử dụng các cơ chế của kernel như Namespaces và cgroups để tạo ra sự cách ly và kiểm soát tài nguyên.
Điểm rất quan trọng:
Container không mang theo kernel riêng như Virtual Machine.
Ví dụ Host chạy một Linux distribution, Container có thể cung cấp user-space của Debian, Ubuntu hoặc một distribution phù hợp với ứng dụng, nhưng các Linux Container vẫn sử dụng kernel của Host.
Đây là lý do Container nhỏ và khởi động nhanh hơn VM.
docker pull – Lấy Image về máy
Muốn chạy Container, Docker Engine trước tiên phải có Image.
Chúng ta có thể chủ động tải Image:
docker pull httpd
Quá trình sẽ là:
Container Registry
│
docker pull
│
▼
Local Image Cache
Nếu chạy docker run mà Image chưa tồn tại local, Docker thông thường cũng sẽ tự động Pull Image cần thiết từ Registry.
docker run – Từ Image thành Container
Lệnh cơ bản:
docker run httpd
Docker sẽ tạo Container từ Image httpd và khởi động Process mặc định đã được định nghĩa trong Image.
Trong thực tế, chúng ta thường cần thêm một số option.
Ví dụ:
docker run -d --name web01 -p 8080:80 httpd
Hãy bóc tách câu lệnh này. -d – Detached Mode
-d
Container chạy dưới background thay vì chiếm Terminal hiện tại.
--name – Đặt tên Container
--name web01
Container sẽ có tên:
web01
Thay vì phải nhớ một Container ID dài, chúng ta có thể quản lý nó bằng tên.
Tên Container cũng hữu ích trong Docker networking: các Container trên cùng một user-defined network có thể phân giải tên của nhau thông qua DNS tích hợp của Docker.
-p – Publish Port
-p 8080:80
Đây là phần người mới học Docker rất dễ nhầm.
Nó có nghĩa:
Client
│
▼
Host:8080
│
▼
Container:80
│
▼
Apache HTTP Server
Port 8080 của Host được publish/map tới Port 80 của Container.
Sau đó người dùng có thể truy cập:
http://<Docker-Host>:8080
Một lưu ý về thuật ngữ: EXPOSE trong Dockerfile chủ yếu mô tả/document port mà ứng dụng dự kiến lắng nghe; còn docker run -p mới thực hiện việc publish port của Container ra Host.
Điều này liên hệ trực tiếp với yếu tố Port Binding trong 12-Factor App mà chúng ta đã học.
-it – Khi muốn tương tác với Container
Đôi khi chúng ta không muốn chạy Web Server mà muốn vào bên trong môi trường Container để kiểm tra.
Có thể sử dụng:
docker run -it ubuntu bash
Trong đó:
-i → Interactive
-t → Allocate pseudo-TTY
và:
bash
là command được yêu cầu chạy trong Container.
Khi đó thay vì khởi động một service theo mặc định của Image, Docker chạy Bash và cho phép chúng ta tương tác với môi trường bên trong Container.
Lưu ý rằng command chỉ chạy được nếu chương trình đó thực sự tồn tại trong Image.
Tại sao nhiều Container Image không có đầy đủ công cụ Linux?
Bạn vào một Container và thử:
ping
vim
curl
ssh
nhưng phát hiện một số lệnh không tồn tại.
Điều này không nhất thiết là lỗi.
Container Image thường được thiết kế theo nguyên tắc:
Chỉ chứa những gì ứng dụng thực sự cần.
Ít package hơn đồng nghĩa với:
- Image nhỏ hơn.
- Download nhanh hơn.
- Build nhanh hơn.
- Ít dependency hơn.
- Giảm Attack Surface.
- Giảm số lượng package cần vá lỗi.
Đó là lý do các Image như slim, alpine hoặc các distroless images thường được sử dụng để tối ưu Container cho Production.
Docker Engine đứng ở đâu trong toàn bộ kiến trúc?
Khi bạn gõ:
docker run ...
Docker CLI không tự mình thực hiện tất cả công việc.
Mô hình tổng quát là:
Docker Client
│
Docker API
│
▼
Docker Engine / Daemon
│
├──── Container Images
│
├──── Containers
│
├──── Networks
│
└──── Volumes
Docker Client gửi yêu cầu tới Docker Engine thông qua API, còn Engine chịu trách nhiệm thực hiện các thao tác liên quan đến Image, Container, Network và Volume.
Kết nối lại với CI/CD
Bây giờ chúng ta có thể nhìn Docker dưới góc độ DevSecOps:
Developer
│
▼
Git Commit
│
▼
CI Pipeline
│
├── Static Analysis
├── Unit Test
└── docker build
│
▼
Docker Image
│
▼
Container Registry
│
docker pull
│
▼
docker run
│
▼
Container
Build Stage tạo Image.
Registry lưu và phân phối Image.
Run Stage tạo Container từ Image.
Và từ cùng một Image đã được kiểm thử, chúng ta có thể tạo nhiều Container giống nhau trên Development, Staging hay Production.
Đây chính là điểm Docker kết nối rất đẹp với những nguyên tắc chúng ta đã học trong chuỗi 12-Factor App: Build–Release–Run, Dev/Prod Parity, Port Binding, Stateless Processes, Concurrency và Disposability.
Hiểu rõ Image ≠ Container là bước đầu tiên để hiểu Docker. Bước tiếp theo quan trọng hơn nữa là hiểu Docker tạo Image như thế nào thông qua Dockerfile và hệ thống Layers, bởi đây chính là nơi chúng ta có thể tối ưu tốc độ Build, kích thước Image và cả bảo mật cho CI/CD Pipeline.