→ From Coder to Shipper
From Coder to Shipper · DevOps

Docker — containers

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

Vấn đề

Chạy được trên máy mình

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.

🧑‍💻 Máy cá nhân của bạn

Python 3.11, đã cài Flask, mọi thứ bạn chỉnh sẵn. App chạy tốt.

✅ chạy

👥 Máy của đồng nghiệp

Python 3.9, các package khác, phiên bản khác. Cùng code đó lại crash.

❌ crash

🖥 Máy server

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

Ý tưởng

Một chiếc hộp nhỏ đồng nhất

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.

Một hộp, mọi thứ bên trong

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.

Cùng hộp, mọi máy

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 phải máy tính mini

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.

Thiết kế để dùng xong bỏ

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.

01
Phần 1

Mô hình tư duy

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.

Mô hình · image và container

Image vs container — công thức vs món ăn

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.

Image — bản thiết kế

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.

Container — một bản đang chạy

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 đó.

Mô hình · hãy hình dung

Một image, nhiều container

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.

image: myapp:1.0
L3code của bạn (app.py)
L2dependencies (flask + thư viện)
L1nền (python 3.11-slim)

bản thiết kế chỉ-đọc

⟶
● container #1 — đang chạy :5000
● container #2 — đang chạy :5000
● container #3 — đang chạy :5000

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

Mô hình · cặp hay nhầm nhất

Container và máy ảo

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.

VM — máy trong máy

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.

Container — app trong hộp

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.

Mô hình · bên trong hộp

Bên trong hộp thực sự có gì

Bốn thứ, và một thứ cố tình không có.

Code của bạn

app.py, templates, file tĩnh — sao vào từ dự án của bạn.

Runtime

Python 3.11 — cái máy thực thi code của bạn, chốt đúng một phiên bản.

Các dependency

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ó: hệ điều hành

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.
toàn bộ mô hình tư duy trong một câu
02
Phần 2

Dockerfile — viết công thức

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 · công thức

Cả một app, trong tám dò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.

Dockerfile
# 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.

Dockerfile · bộ công cụ

Năm lệnh bạn sẽ dùng 95%

Docker có khoảng 15 lệnh. Năm lệnh này đủ cho gần như mọi app.

LệnhViệc của nóVí dụ
FROMBắt đầu từ một image nền — lớp móngFROM python:3.11-slim
COPYSao file của bạn vào hộpCOPY app.py /app/
RUNCài & chuẩn bị — xảy ra lúc build, đóng băng vào imageRUN pip install flask
EXPOSEGhi chú app lắng nghe cổng nàoEXPOSE 5000
CMDLệnh mặc định chạy khi container khởi độngCMD ["python", "app.py"]
Dockerfile · hai cái đồng hồ

Lúc build vs lúc chạy

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.

RUN — lúc build

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.

CMD — lúc khởi động

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.

Các lớp được cache

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.

Dockerfile · lần build

docker build thực sự làm gì

Bốn nhịp giữa một thư mục code và một image đã đóng hộp.

  1. 1

    Đọc Dockerfile

    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.

  2. 2

    Lớp nền

    Kéo FROM python:3.11-slim từ registry — hoặc dùng bản cục bộ nếu đã có.

  3. 3

    Thực thi từng dòng

    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.

  4. 4

    Đánh dấu kết quả

    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.

Dockerfile · trong terminal

Build nó

Một lệnh biến thư mục thành image đã đóng hộp.

ops@vps — zsh
$ docker build -t myapp:1.0 .
=> [1/6] FROM docker.io/library/python:3.11-slim
=> CACHED [2/6] WORKDIR /app
=> [5/6] RUN pip install -r requirements.txt
$ docker images
REPOSITORY TAG SIZE
myapp 1.0 148MB
the folder is now a boxed image, stored locally
03
Phần 3

Chạy một container

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ì.

Chạy · hai lệnh

Hai lệnh quan trọng nhất

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.

bash
# 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".

Chạy · các cờ

Bốn cờ, 90% công việc

Học bốn cờ này là đọc được mọi lệnh docker run bạn sẽ gặp.

FlagNó 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 đượccurl http://127.0.0.1:8080
-v name:/pathVolume: 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
-dDetach: chạy nền, in ra ID containerdocker run -d …
--name webĐặt tên thay vì ID ngẫu nhiêndocker ps
Chạy · trong terminal

Chạy và quan sát

Container chạy nền; chính port map là thứ khiến nó tiếp cận được.

ops@vps — zsh
$ docker run -d --name web -p 8080:5000 myapp:1.0
a1b2c3d4e5f6
$ docker ps
CONTAINER ID IMAGE STATUS PORTS
a1b2c3d4e5f6 myapp:1.0 Up 5 seconds 0.0.0.0:8080->5000/tcp
$ curl http://127.0.0.1:8080/
<h1>Hello, shipper!</h1>
host port 8080 knocked; container port 5000 answered
Chạy · cách ly

Container nhìn thấy gì

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.

CONTAINER — VŨ TRỤ NHỎ RIÊNG
thư mục / riêng — app.py · python · flask
process riêng — PID 1 = python app.py
mạng riêng — IP 172.17.0.2 · port 5000
🚪 8080 → 5000
(port map)
⟷

🖥 Máy host

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

Chạy · cách ly

Vì sao cách ly quan trọng

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.

Hệ file riêng

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.

Process riêng

PID 1 là app của bạn. Process host và process container mặc định không thấy nhau.

Mạng riêng

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.

Thay thế rẻ

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.

Chạy · cổng

EXPOSE và -p — hai thứ khác nhau

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.

EXPOSE — ghi chú

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.

-p — cánh cửa thật

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ó.

04
Phần 4

docker compose — một gia đình container

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.

Compose · file

Một file, cả một hệ thống

App Flask của chúng ta cùng database. Cùng app như trước — giờ có thêm Postgres thật.

docker-compose.yml
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.

Compose · các key

Năm key của compose

Cả file đó là năm ý tưởng. Học các key là mọi file compose đều đọc được.

services

Mỗi mục dưới services: là một container. Ở đây: web và db.

ports

Cùng phép ánh xạ host:container như -p, viết trong file.

volumes

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.

depends_on

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.)

environment

Truyền cấu hình qua biến môi trường — cùng việc như -e.

Compose · trong terminal

Một lệnh, cả hệ thống

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.

ops@vps — zsh
$ docker compose up -d
✔ Network docker_default Created
✔ Container docker-db-1 Started
✔ Container docker-web-1 Started
$ docker compose ps
web Up 0.0.0.0:8080->5000/tcp
db Up 5432/tcp
$ docker compose down # remove the containers
Compose · hãy hình dung

Gia đình đang chạy

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.

MÁY HOST
web — app Flask lắng nghe :5000
mạng nội bộ
web → db:5432 (theo tên)
db — Postgres lắng nghe :5432
🗄 db-data — volume trên đĩa host · sống qua rebuild

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.

05
Phần 5

Dữ liệu sống ở đâu

Đ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.

Dữ liệu · hai hệ file

Hai hệ file, hai cuộc đời khác nhau

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.

Hệ file của container

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.

Một volume

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ó.

Dữ liệu · quy tắc vàng

Cái nào sống qua cái nào

Bảng quan trọng nhất trong khóa slide này. Ghi nhớ cột bên phải.

Thao tácChuyện gì xảy ra
docker stop / startApp tạm dừng; mọi thứ nguyên vẹn
docker compose downXóa container; volume sống
up --build after a code changeCode mới chạy; dữ liệu cũ vẫn nằm trong volume
docker compose down -vXóa cả container lẫn volume — dữ liệu mất thật sự
Dữ liệu · trong terminal

Xem dữ liệu sống lâu hơn container

Đập cả hệ thống xuống rồi dựng lại. Container mới; ký ức của nó thì không.

ops@vps — zsh
$ docker compose up -d
$ docker compose down
$ docker volume ls
LOCAL VOLUME
docker_db-data
$ docker compose up -d
the container is new; its data is not
Toàn cảnh · ship

Cùng một hộp, từ máy cá nhân đến production

Đâ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.

🌐 Trình duyệt

▶

🚪 nginx

cánh cửa trước — HTTPS, file tĩnh, chuyển tiếp. Chủ đề tiếp theo.

▶
🐳 container: app Flask + Python + deps
🗄 myapp-data — volume (dữ liệu)
💻 máy cá nhân
image: myapp:1.0
▶
🧪 server test
image: myapp:1.0
▶
🚀 production
image: myapp:1.0

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)

Toàn cảnh · ngoài phạm vi

Những gì cố tình bỏ qua

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.

Multi-stage build

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.

Chế độ mạng

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.

Kubernetes & Swarm

Đ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.

Registry image

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.
đạo lý của người shipper

Build. Run. Ship.

Tiếp theo: nginx — cánh cửa trước đứng ngay phía trước container.