Gói ứng dụng cùng mọi thứ nó cần vào một chiếc hộp nhỏ — và ship cùng một hộp đó từ máy cá nhân đến server production.
← / → để chuyển slide · chuyển EN/VI bên dưới
Câu nói nổi tiếng nhất trong ngành phần mềm — và là lý do container tồn tại. Cùng code, máy khác nhau, kết quả khác nhau.
Python 3.11, đã cài Flask, mọi thứ bạn chỉnh sẵn. App chạy tốt.
✅ chạy
Python 3.9, các package khác, phiên bản khác. Cùng code đó lại crash.
❌ crash
Không cài gì cả. "Chạy trên máy mình" không phải sách hướng dẫn.
❌ bật lại
code giống hệt nhau · môi trường thì không — và chính môi trường là thứ làm hỏng việc
Container đóng gói app cùng mọi thứ nó cần, để chiếc hộp — chứ không phải cái máy — quyết định app hoạt động ra sao.
Code của bạn, runtime ngôn ngữ (Python), mọi dependency, và cấu hình — đóng băng trong một chiếc hộp duy nhất, di chuyển được.
Máy cá nhân, server test, production: đều chạy cùng một hộp, nên app hoạt động y hệt. Cái máy bên dưới vô hình với app.
Không có hệ điều hành bên trong. Container mượn kernel OS của máy chủ và chỉ mang app cùng những gì cần — vì thế nó nhỏ và nhanh.
Container dừng, xóa, tạo lại rất rẻ. Hỏng thì không sửa — thay bằng bản sao mới của cùng một hộp.
Image so với container, bên trong hộp thực sự có gì, và container khác máy ảo chỗ nào.
Cặp từ quan trọng nhất trong Docker. Hiểu đúng hai từ này, phần còn lại tự theo.
Công thức chỉ-đọc: mẩu OS cơ bản + runtime + code của bạn + toàn bộ dependency, build một lần từ Dockerfile. Lưu được, chia sẻ được, không đổi.
Hãy tưởng tượng: khuôn bánh đông đá — là hình dạng, không phải cái bánh.
Bản thiết kế được thổi hồn: một process đang chạy, file riêng, mạng riêng. Khởi động, dừng, xóa — image không bao giờ bị động tới.
Hãy tưởng tượng: bao nhiêu cái bánh cắt từ cùng khuôn đó.
Bạn build một image. Rồi chạy được bao nhiêu bản tùy ý — chúng không làm phiền nhau.
bản thiết kế chỉ-đọc
image = công thức · container = một món nấu từ nó · nhiều món từ một công thức
Cả hai đều "chạy phần mềm tách biệt" — nhưng ảo hóa những thứ khác nhau. Sự khác nhau đó giải thích toàn bộ chuyện kích thước và tốc độ của container.
Hypervisor giả lập cả phần cứng; máy khách chạy một hệ điều hành đầy đủ riêng, khởi động như máy thật, nặng vài GB.
Ảo hóa: phần cứng. Khởi động trong vài phút.
Không giả phần cứng, không OS khách. Process của container chạy thẳng trên kernel của máy chủ, chỉ mang app + dependency — vài MB.
Ảo hóa: hệ điều hành (kernel dùng chung). Khởi động trong vài mili giây.
Bốn thứ, và một thứ cố tình không có.
app.py, templates, file tĩnh — sao vào từ dự án của bạn.
Python 3.11 — cái máy thực thi code của bạn, chốt đúng một phiên bản.
Flask và mọi thư viện nó kéo theo, cài phiên bản chính xác lúc build.
Không có phân phối Linux bên trong. Hộp dùng chung kernel của máy chủ — đó là lý do nó nhỏ và nhanh.
Chạy được trong hộp trên máy mình thì chạy được trong hộp trên server. Hộp không đổi — chỉ cái máy bên dưới là đổi.
Vài dòng text ngắn nói cho Docker biết chính xác phải cho gì vào hộp và chạy gì khi khởi động.
Dockerfile này đóng gói app Flask của chủ đề trước. Đọc từ trên xuống: mỗi dòng thêm một lớp.
# recipe for our Flask app
FROM python:3.11-slim # start from a ready-made base
WORKDIR /app
COPY app.py . # put your code in
COPY requirements.txt .
RUN pip install -r requirements.txt # install deps
EXPOSE 5000 # document the port
CMD ["python", "app.py"] # what to run at start
Dòng đầu tiên là dòng duy nhất bắt buộc. Phần còn lại là gia vị tùy chọn.
Docker có khoảng 15 lệnh. Năm lệnh này đủ cho gần như mọi app.
| Lệnh | Việc của nó | Ví dụ |
|---|---|---|
| FROM | Bắt đầu từ một image nền — lớp móng | FROM python:3.11-slim |
| COPY | Sao file của bạn vào hộp | COPY app.py /app/ |
| RUN | Cài & chuẩn bị — xảy ra lúc build, đóng băng vào image | RUN pip install flask |
| EXPOSE | Ghi chú app lắng nghe cổng nào | EXPOSE 5000 |
| CMD | Lệnh mặc định chạy khi container khởi động | CMD ["python", "app.py"] |
Chỗ hay nhầm nhất trong Docker: chuyện gì xảy ra khi build, và khi run. Hai cái đồng hồ khác nhau.
pip install chạy một lần khi build image; kết quả đóng băng vào một lớp. Bạn không bao giờ thấy nó chạy lại — trừ khi build lại.
Công việc của container, thực thi mỗi lần container khởi động. Đây là app của bạn: python app.py.
Chỉ sửa app.py? Docker tái dụng mọi lớp trước và chỉ build lại từ COPY trở đi. Build lại có thể chỉ vài giây.
Bốn nhịp giữa một thư mục code và một image đã đóng hộp.
Từ trên xuống, từng dòng. Mỗi dòng là một bước trong khâu lắp ráp.
Kéo FROM python:3.11-slim từ registry — hoặc dùng bản cục bộ nếu đã có.
COPY sao, RUN cài… mỗi dòng xong trở thành một lớp đã lưu, chồng lên lớp trước.
Cả chồng lớp là image của bạn, lưu cục bộ dưới tên myapp:1.0 — sẵn sàng để chạy.
Một lệnh biến thư mục thành image đã đóng hộp.
Image vốn trơ cho đến khi ai đó run nó. docker run thổi hồn cho nó — và vài cờ quyết định nó thấy được gì.
Build làm ra hộp; run làm ra một bản đang chạy của hộp. Đây là 90% Docker hàng ngày.
# 1. turn the Dockerfile into an image
docker build -t myapp:1.0 .
# 2. turn the image into a running container
docker run \
-d --name web \
-p 8080:5000 \
-v myapp-data:/app/data \
myapp:1.0
-t đánh tên image; dấu chấm cuối nghĩa là "đọc Dockerfile từ thư mục này".
Học bốn cờ này là đọc được mọi lệnh docker run bạn sẽ gặp.
| Flag | Nó làm gì | Ví dụ |
|---|---|---|
| -p 8080:5000 | Ánh xạ cổng: cổng host → cổng container — khiến app tiếp cận được | curl http://127.0.0.1:8080 |
| -v name:/path | Volume: gắn đĩa lưu trữ của host vào hộp để dữ liệu sống lâu | -v db-data:/var/lib/postgres |
| -d | Detach: chạy nền, in ra ID container | docker run -d … |
| --name web | Đặt tên thay vì ID ngẫu nhiên | docker ps |
Container chạy nền; chính port map là thứ khiến nó tiếp cận được.
Bên trong, container là một vũ trụ nhỏ riêng. Bên ngoài, host tiếp cận nó chỉ qua một cánh cửa duy nhất.
Máy cá nhân hay server của bạn: file riêng, process riêng, cổng riêng. Chỉ chui được vào trong qua cổng đã ánh xạ.
container không thấy file hay process của host; host tiếp cận nó chỉ qua cổng đã ánh xạ — đó là sự cách ly
Ba bức tường, một kết quả: container không phá được host, và host không lẻn vào được container.
Thư mục / của container là image. /usr và /etc của host đơn giản là không thấy được từ bên trong.
PID 1 là app của bạn. Process host và process container mặc định không thấy nhau.
IP riêng tư và bảng cổng riêng. Ra bên ngoài chỉ qua cổng đã ánh xạ hoặc mạng compose.
Vì không gì rò rỉ ra/vào, container có thể xóa và tạo lại trong vài giây — lại là cùng một hộp.
Một thứ là ghi chú; thứ kia là cánh cửa. Nhầm hai thứ này là lỗi cổng phổ biến nhất trong Docker.
Ghi chú trong image: "app lắng nghe ở 5000". Tự nó, nó không mở gì — host vẫn không tiếp cận container.
Có ích cho người đọc và cho compose. Nó không phải ánh xạ cổng.
docker run -p 8080:5000 nối cổng host với cổng container. Lưu lượng vào 8080 của host đáp xuống 5000 bên trong.
Không có -p, chỉ các container khác (qua mạng compose) mới tiếp cận được nó.
App thật hiếm khi là một hộp: web app + database + (sau này) nginx. Compose mô tả cả gia đình trong một file và khởi động bằng một lệnh.
App Flask của chúng ta cùng database. Cùng app như trước — giờ có thêm Postgres thật.
services:
web:
build: .
ports:
- "8080:5000"
depends_on:
- db
db:
image: postgres:16
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
web build từ Dockerfile của chúng ta; db là có sẵn từ registry.
Cả file đó là năm ý tưởng. Học các key là mọi file compose đều đọc được.
Mỗi mục dưới services: là một container. Ở đây: web và db.
Cùng phép ánh xạ host:container như -p, viết trong file.
Lưu trữ đặt tên, sống trên host và sống qua các lần rebuild container — nhà của database.
Gợi ý thứ tự: khởi động db trước web. (Là gợi ý — không đảm bảo DB đã sẵn sàng hẳn.)
Truyền cấu hình qua biến môi trường — cùng việc như -e.
Up đưa cả gia đình vào đời sống; down thu dọn. Volume thì ở lại trong cả hai trường hợp.
Các service trong cùng file compose nói chuyện với nhau bằng tên — không IP, không mở cổng, không firewall.
từ bên trong, web chỉ cần nối tới db:5432 — tên service. Docker tự giải quyết. Vì thế container không cần cấu hình IP.
Điều duy nhất phải sống lâu hơn hộp: database của bạn. Container dùng xong bỏ; dữ liệu thì không.
Cùng một hộp có hai loại lưu trữ, và chúng chết vào những thời điểm rất khác nhau.
Image chỉ-đọc cộng một lớp ghi mỏng. Xóa container là lớp đó biến mất — file database cũng theo.
Tuổi thọ: bằng tuổi container.
Lưu trữ đặt tên trên đĩa host, gắn vào trong hộp. Sống qua stop, restart, xóa — và cả rebuild image.
Tuổi thọ: cho đến khi ai đó chủ động xóa nó.
Bảng quan trọng nhất trong khóa slide này. Ghi nhớ cột bên phải.
| Thao tác | Chuyện gì xảy ra |
|---|---|
| docker stop / start | App tạm dừng; mọi thứ nguyên vẹn |
| docker compose down | Xóa container; volume sống |
| up --build after a code change | Code mới chạy; dữ liệu cũ vẫn nằm trong volume |
| docker compose down -v | Xóa cả container lẫn volume — dữ liệu mất thật sự |
Đập cả hệ thống xuống rồi dựng lại. Container mới; ký ức của nó thì không.
Đây là phần hái quả: app Flask của bạn thành một image, và chính image đó — không phải một thư mục code — là thứ bạn ship.
cánh cửa trước — HTTPS, file tĩnh, chuyển tiếp. Chủ đề tiếp theo.
build một lần trên máy cá nhân · cùng những byte đó chạy trên server test và production — với nginx ở phía trước (chủ đề sau)
Container có một hộp công cụ rất sâu. Bốn thứ sau là thật — nhưng không phải bước tiếp theo của bạn.
Mẹo công thức: build ở một giai đoạn béo bở, copy kết quả vào image cuối nhỏ xíu. Bạn sẽ gặp khi image bắt đầu to.
bridge, host, mạng tùy chỉnh… compose đã cho bạn 95% điều cần; phần còn lại dành cho các setup đặc biệt.
Điều phối nhiều container trên nhiều máy. Một lĩnh vực khác (rất hấp dẫn) — không phải con đường của khóa này.
Docker Hub & bạn bè: nơi image được lưu và chia sẻ giữa các máy. Mình build cục bộ trước — registry sẽ đến sau.
Bạn không ship code nữa. Bạn ship một chiếc hộp. Cùng một hộp trên máy cá nhân, trên server test, và trong production — đó là toàn bộ câu chuyện.
Tiếp theo: nginx — cánh cửa trước đứng ngay phía trước container.