Danh sách bài viết

Bài 32: Docker cơ bản — image, container, Dockerfile

Docker giải quyết vấn đề "chạy được trên máy tôi nhưng lỗi trên server" bằng cách đóng gói toàn bộ môi trường — Python version, thư viện, CUDA — vào một image di động. Bài này đi qua lý do AI Engineer cần Docker, 3 khái niệm cốt lõi (image, container, Dockerfile), cài đặt, các lệnh hay dùng nhất, viết Dockerfile đầu tiên, tối ưu layer cache, .dockerignore, và chọn base image phù hợp cho CPU/GPU inference.

27/05/2026
1 lượt xem
1

Mục tiêu bài học

Sau khi hoàn thành bài này, bạn sẽ:

  • ✅ Hiểu vì sao Docker giải quyết được vấn đề môi trường trong AI project
  • ✅ Phân biệt được image, container, Dockerfile
  • ✅ Cài Docker và verify hoạt động
  • ✅ Dùng được các lệnh docker hay gặp nhất
  • ✅ Viết và build một Dockerfile Python cơ bản
  • ✅ Hiểu cơ chế layer cache và cách sắp xếp instruction để tối ưu
  • ✅ Chọn được base image phù hợp cho CPU hoặc GPU inference
2

Vì sao Docker cho AI Engineer

Model AI phụ thuộc vào nhiều thứ cùng lúc: phiên bản Python, thư viện (PyTorch, transformers, numpy), driver CUDA, và đôi khi cả hệ điều hành. Trên máy dev mọi thứ khớp nhau. Nhưng khi deploy lên server hoặc chuyển sang máy đồng nghiệp, chỉ cần lệch một thư viện là lỗi.

Reproducibility

Docker đóng gói toàn bộ môi trường vào image: Python 3.11.7, PyTorch 2.3.0, CUDA 12.1, các lib cụ thể với version pin. Ai pull image đó cũng chạy được, trên bất kỳ máy nào có Docker. Không còn "works on my machine".

Portability

Image Docker chạy được trên laptop macOS, server Ubuntu, cloud VM (AWS EC2, GCP Compute Engine, Azure VM) mà không cần cấu hình lại. Các platform PaaS như Render, Railway, Fly.io đều nhận Docker image trực tiếp.

Isolation

Nhiều project AI cùng lúc thường conflict dependency: project A dùng PyTorch 1.13 (cần CUDA 11.6), project B dùng PyTorch 2.3 (cần CUDA 12.1). Với Docker, mỗi project chạy trong container riêng, không ảnh hưởng nhau. Không cần virtualenv hoặc conda juggling phức tạp.

Deployment

Mỗi platform chỉ cần một Docker image. Build một lần, deploy lên Render, Railway, AWS ECS, hoặc Kubernetes — cùng một image, không thêm config environment-specific nào.

3

3 khái niệm cốt lõi: Image, Container, Dockerfile

Image

Image là snapshot read-only của filesystem kèm config (biến môi trường, port, command mặc định). Nó là bản thiết kế, không chạy trực tiếp được. Phép so sánh thường dùng: image giống class trong OOP — định nghĩa cấu trúc nhưng bản thân không "sống".

Image được tổ chức thành các layer xếp chồng nhau. Mỗi instruction trong Dockerfile tạo ra một layer. Layer chỉ đọc và có thể được chia sẻ giữa nhiều image (tiết kiệm disk).

Container

Container là một instance đang chạy (hoặc đã dừng) của image — giống object được tạo từ class. Docker thêm một layer writable lên trên các layer read-only của image. Khi container bị xóa, layer writable đó biến mất cùng. File muốn lưu lại phải dùng volume.

Nhiều container có thể chạy cùng lúc từ một image, độc lập nhau.

Dockerfile

Dockerfile là file text thuần chứa danh sách instruction để Docker build image. Mỗi instruction mô tả một bước: dùng base image nào, copy file gì, chạy lệnh install nào, expose port nào, command gì khi khởi động.

Quan hệ giữa ba khái niệm:

Dockerfile  --[docker build]-->  Image  --[docker run]-->  Container
4

Cài đặt Docker

macOS

Cài Docker Desktop từ trang chính thức: https://docs.docker.com/desktop/install/mac-install/

Docker Desktop bao gồm Docker Engine, Docker CLI, Docker Compose và một daemon quản lý qua GUI. Sau khi cài, Docker Desktop cần chạy ở background (icon trên menu bar) trước khi dùng CLI.

Lưu ý: Docker Desktop cho macOS chạy Linux VM bên dưới — container chạy Linux, không phải macOS native. Điều này không ảnh hưởng hầu hết use case AI.

Windows

Cài Docker Desktop với WSL2 backend: https://docs.docker.com/desktop/install/windows-install/

WSL2 (Windows Subsystem for Linux 2) cần được bật trước. Docker Desktop sẽ hướng dẫn từng bước. Sau khi cài, dùng CLI trong PowerShell hoặc Windows Terminal.

Linux (Ubuntu / Debian)

Cách nhanh nhất:

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Logout và login lại để group có hiệu lực

Script trên cài Docker Engine (không có GUI). Sau khi thêm user vào group docker, bạn không cần sudo trước mỗi lệnh docker nữa.

Nếu muốn cài thủ công theo official guide: https://docs.docker.com/engine/install/ubuntu/

Verify cài đặt

docker --version
# Docker version 26.1.4, build 5650f9b

docker run hello-world

Output của hello-world sẽ in "Hello from Docker!" kèm giải thích Docker đã làm gì để chạy container đó. Nếu thấy output này thì Docker hoạt động bình thường.

5

Lệnh cơ bản

Lệnh Tác dụng
docker pull <image> Tải image từ registry (Docker Hub mặc định) về local
docker images Liệt kê image đang có ở local
docker run <image> Tạo container mới từ image và khởi động nó
docker ps Liệt kê container đang chạy
docker ps -a Liệt kê tất cả container (cả đã dừng)
docker stop <id> Dừng container (gửi SIGTERM, chờ 10s rồi SIGKILL)
docker rm <id> Xóa container đã dừng
docker rmi <image> Xóa image khỏi local
docker exec -it <id> bash Mở shell bên trong container đang chạy
docker logs <id> Xem stdout/stderr của container
docker logs -f <id> Follow log theo thời gian thực
docker inspect <id> Xem metadata chi tiết của container (JSON)

ID container: bạn không cần gõ full 64 ký tự — chỉ cần vài ký tự đầu đủ phân biệt là Docker nhận ra.

Dọn dẹp

# Xóa tất cả container đã dừng
docker container prune

# Xóa image không còn được dùng bởi container nào
docker image prune

# Dọn triệt để: container, image, network, build cache không dùng
docker system prune -a

AI image thường nặng vài GB. Sau vài tuần thử nghiệm, docker system prune -a có thể giải phóng 20–50 GB.

6

Chạy container đầu tiên

docker run -it python:3.11-slim python

Docker sẽ pull image python:3.11-slim (~120 MB) nếu chưa có, tạo container, và khởi động REPL Python bên trong container. Bạn có thể gõ code Python ngay trong terminal.

  • -i (interactive): giữ stdin mở để có thể nhập lệnh.
  • -t (tty): tạo pseudo-terminal, cho phép hiển thị đúng prompt.
  • Hai flag này thường viết gộp là -it.

Thử trong REPL:

import sys
print(sys.version)
# 3.11.x (main, ...) [GCC ...]

import torch  # ImportError: No module named 'torch'

Image python:3.11-slim chỉ có Python và stdlib — PyTorch chưa được cài. Đây là điểm quan trọng: image là môi trường tối giản, bạn phải khai báo mọi dependency trong Dockerfile.

Thoát REPL bằng exit() hoặc Ctrl+D. Container sẽ dừng tự động.

7

Port forward, volume mount và env variable

docker run -d \
  --name my-api \
  -p 8000:8000 \
  -v $(pwd)/data:/app/data \
  -e API_KEY=secret \
  python:3.11-slim sleep infinity

Giải thích từng flag:

  • -d (detached): chạy nền, trả terminal về ngay. Container vẫn chạy.
  • --name my-api: đặt tên container. Thay vì dùng ID, bạn có thể gõ docker stop my-api.
  • -p 8000:8000: forward port. Format host_port:container_port. Request đến localhost:8000 trên máy bạn sẽ được chuyển vào port 8000 bên trong container.
  • -v $(pwd)/data:/app/data: mount volume. Thư mục ./data trên host được ánh xạ vào /app/data bên trong container. File tạo trong container tại /app/data sẽ xuất hiện ở ./data trên host (và ngược lại).
  • -e API_KEY=secret: đặt biến môi trường. Dùng os.environ["API_KEY"] để đọc trong code Python. Không hard-code secret vào image.
  • sleep infinity: command chạy bên trong container. Container này chỉ đứng yên để bạn exec vào kiểm tra.

Sau khi container chạy, vào bên trong:

docker exec -it my-api bash
# Bây giờ bạn đang ở trong container
ls /app/data  # thấy file từ host
echo $API_KEY  # secret
exit

Dừng và xóa container:

docker stop my-api
docker rm my-api
8

Viết Dockerfile đầu tiên

Giả sử project structure:

my-ai-app/
├── app.py
├── requirements.txt
└── Dockerfile

requirements.txt:

fastapi==0.111.0
uvicorn[standard]==0.30.1

Dockerfile:

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

Giải thích từng instruction:

  • FROM python:3.11-slim: bắt đầu từ base image. Mọi Dockerfile đều có đúng một instruction FROM (trừ multi-stage build ở bài 34).
  • WORKDIR /app: đặt working directory cho các instruction tiếp theo và khi container khởi động. Nếu thư mục chưa có, Docker tự tạo.
  • COPY requirements.txt .: copy file từ build context (thư mục chứa Dockerfile) vào image tại WORKDIR. Dấu ./app (WORKDIR hiện tại).
  • RUN pip install --no-cache-dir -r requirements.txt: chạy lệnh này khi build image. --no-cache-dir tránh lưu pip cache vào image (~200 MB thêm).
  • COPY . .: copy toàn bộ source code vào image.
  • EXPOSE 8000: document rằng container này sẽ lắng nghe port 8000. Không tự forward port ra host — phải dùng -p khi docker run.
  • CMD [...]: command chạy khi container khởi động. Dùng JSON array form (exec form) thay vì shell form để signal handling hoạt động đúng.

Lưu ý --host 0.0.0.0: Uvicorn mặc định bind 127.0.0.1 — chỉ accessible từ trong container. Muốn port forward ra ngoài hoạt động, phải bind 0.0.0.0.

9

Build và chạy image

# Đứng ở thư mục chứa Dockerfile
docker build -t my-ai-app:0.1 .
  • -t my-ai-app:0.1: gán tên và tag cho image. Format name:tag. Tag thường là version.
  • .: build context — Docker sẽ gửi toàn bộ nội dung thư mục này lên build daemon. Thư mục . là thư mục hiện tại. Dùng .dockerignore để loại trừ file không cần thiết (xem mục 11).

Output build sẽ hiện từng step tương ứng với instruction trong Dockerfile. Bước cuối cùng sẽ in ID của image vừa tạo.

# Chạy container từ image
docker run -d -p 8000:8000 --name my-api my-ai-app:0.1

# Kiểm tra
curl http://localhost:8000

Nếu curl trả về response từ FastAPI thì pipeline hoạt động đúng. Xem log nếu có lỗi:

docker logs my-api
# hoặc theo dõi live
docker logs -f my-api
10

Layer và cache — tối ưu build time

Mỗi instruction trong Dockerfile tạo ra một layer. Docker cache lại từng layer. Khi build lần sau, nếu instruction đó và input không đổi so với lần trước, Docker reuse cache thay vì chạy lại. Nhưng nếu một layer bị invalidate, mọi layer sau nó đều rebuild.

Thứ tự instruction quan trọng

Cách xấu — build chậm:

FROM python:3.11-slim
WORKDIR /app

# Layer này bao gồm cả source code
COPY . .

# Layer này rebuild mỗi khi code thay đổi, dù requirements không đổi
RUN pip install --no-cache-dir -r requirements.txt

Cách tốt — tận dụng cache:

FROM python:3.11-slim
WORKDIR /app

# Copy chỉ file requirements — ít thay đổi
COPY requirements.txt .
# Layer này được cache miễn là requirements.txt không đổi
RUN pip install --no-cache-dir -r requirements.txt

# Copy source code — hay thay đổi nhất, để cuối
COPY . .

Với cách tốt: khi bạn chỉ sửa code mà không thêm/bớt dependency, Docker reuse cache ở bước pip install (thường mất 1–3 phút) và chỉ rebuild layer COPY . . cuối. Build time giảm từ vài phút xuống vài giây.

Quy tắc sắp xếp instruction

Đặt instruction theo thứ tự từ ít thay đổi đến hay thay đổi nhất:

  1. FROM — base image, rất ít khi đổi
  2. Cài system dependencies (apt-get) — đổi khi cần lib hệ thống mới
  3. COPY requirements.txt + pip install — đổi khi thêm/bớt package
  4. COPY . . — đổi mỗi khi sửa code
11

.dockerignore

.dockerignore là file text đặt cùng thư mục với Dockerfile, liệt kê pattern file/thư mục KHÔNG được copy vào build context. Cú pháp giống .gitignore.

File .dockerignore điển hình cho AI project:

__pycache__/
*.pyc
*.pyo
.git/
.github/
.venv/
venv/
*.log
*.egg-info/
.pytest_cache/
.mypy_cache/
.ruff_cache/

# Quan trọng: dataset không vào image
data/
datasets/
*.csv
*.parquet
*.jsonl

# Model weights (dùng download script thay vì bundle vào image)
*.bin
*.safetensors
*.pt
*.pth
*.onnx

# Secret files
.env
.env.*
secrets.json

Hai lý do chính:

  • Kích thước image: Dataset 10 GB hoặc model weight 7 GB không nên nằm trong image. Kéo image như vậy tốn vài phút mỗi lần deploy. Model nên được download từ Hugging Face Hub, S3, hoặc mount volume khi runtime.
  • Bảo mật: .env chứa API key, database password. Nếu vào image, mọi người pull image đó đều đọc được. Truyền secret qua biến môi trường khi docker run -e hoặc dùng secret manager.

Cũng tránh copy .git/ — thư mục này có thể vài trăm MB và không cần thiết khi chạy app.

12

Docker Hub và image registry

Docker Hub (https://hub.docker.com) là registry công khai mặc định. Khi bạn docker pull python:3.11-slim, Docker lấy image từ đây. Image công khai pull miễn phí và không giới hạn.

Push image lên Docker Hub

# Login
docker login

# Tag image với username Docker Hub của bạn
docker tag my-ai-app:0.1 yourusername/my-ai-app:0.1

# Push
docker push yourusername/my-ai-app:0.1

Sau khi push, ai cũng có thể docker pull yourusername/my-ai-app:0.1 nếu là public repo. Private repo cần paid plan (1 repo private miễn phí cho personal account).

Alternatives

  • GitHub Container Registry (ghcr.io): tích hợp với GitHub Actions, tiện cho CI/CD. Miễn phí cho public repo.
  • AWS ECR (Elastic Container Registry): dùng khi deploy lên AWS ECS hoặc EKS. Tính phí theo GB lưu trữ và data transfer.
  • Google Artifact Registry: tương đương ECR cho GCP. Thay thế Container Registry (đã deprecated).
  • Hugging Face Docker Spaces: đặc biệt phù hợp cho demo AI — bạn push Dockerfile, Spaces tự build và host.
13

Base image cho AI/ML

Chọn base image ảnh hưởng trực tiếp đến kích thước image cuối, thời gian pull, và khả năng chạy GPU.

Base image Kích thước (approx) Dùng khi
python:3.11-slim ~120 MB CPU inference, API không cần GPU
python:3.11 ~900 MB Cần nhiều system lib (gcc, etc.)
python:3.11-alpine ~50 MB Rất nhỏ, nhưng nhiều lib Python gặp lỗi compile với musl libc
nvidia/cuda:12.1-runtime-ubuntu22.04 ~1.5 GB Cần CUDA runtime, tự cài Python + PyTorch
pytorch/pytorch:2.3.0-cuda12.1-cudnn8-runtime ~4 GB PyTorch + CUDA đã cài sẵn, tiết kiệm thời gian build
huggingface/transformers-pytorch-gpu ~7 GB Transformers + PyTorch + CUDA, dùng nhanh cho prototype

Khuyến nghị theo use case

FastAPI serving model nhỏ trên CPU (bài 1–8 trong series này):

FROM python:3.11-slim

Inference với PyTorch trên GPU (server có NVIDIA GPU):

FROM pytorch/pytorch:2.3.0-cuda12.1-cudnn8-runtime

Image này nặng ~4 GB nhưng đã có CUDA + cuDNN + PyTorch. Bạn chỉ cần pip install thêm các lib còn thiếu.

Không dùng latest tag: python:latest hay pytorch/pytorch:latest không xác định chính xác version nào — khi base image update lên major version mới, build của bạn có thể bị break. Luôn pin tag cụ thể như python:3.11.9-slim hoặc pytorch/pytorch:2.3.0-cuda12.1-cudnn8-runtime.

CUDA version phải khớp driver

Image có CUDA 12.1 cần driver NVIDIA phiên bản ≥ 530.30.02 trên host. Nếu driver cũ hơn, container sẽ không thấy GPU. Kiểm tra driver version trên host:

nvidia-smi
# Driver Version: 535.104.05    CUDA Version: 12.2

CUDA version trong output nvidia-smi là maximum CUDA version mà driver đó hỗ trợ. Image có CUDA ≤ version đó là OK.

14

Common pitfalls

1. Dùng latest tag

Đã đề cập ở trên. Hậu quả: build hôm nay khác build tuần sau vì base image đã update. Pin version cụ thể.

2. Quên .dockerignore

Copy .git/ (vài trăm MB) hoặc .venv/ (vài trăm MB) vào build context làm tăng kích thước image vô ích và làm chậm quá trình gửi context lên build daemon.

3. Layer apt-get tách rời

Xấu:

RUN apt-get update
RUN apt-get install -y libopenblas-dev
RUN apt-get install -y libomp-dev

Ba layer riêng → apt cache từ apt-get update ở layer 1 không được dọn sạch → image phình to. Nếu layer 1 cache hit (không chạy lại), layer 2 có thể dùng package list cũ.

Tốt:

RUN apt-get update && apt-get install -y \
    libopenblas-dev \
    libomp-dev \
    && rm -rf /var/lib/apt/lists/*

Gộp một lệnh, dọn apt cache ngay trong cùng layer → không thêm rác vào image.

4. Chạy container với user root

Mặc định container chạy là root. Nếu có lỗ hổng trong app, attacker có full quyền root bên trong container. Dùng non-root user:

RUN useradd --create-home appuser
USER appuser

5. pip install không --no-cache-dir

pip cache chiếm ~100–300 MB trong image. Không có giá trị gì vì image không pip install thêm khi runtime. Luôn thêm --no-cache-dir.

6. Uvicorn bind 127.0.0.1

Đã đề cập: phải bind 0.0.0.0 thì port forward mới hoạt động. Lỗi này rất phổ biến — curl localhost:8000 trên host không có gì trả về dù container đang chạy.

7. Volume mount sai chiều

Format của -vhost_path:container_path. Nhầm chiều sẽ không thấy file. Kiểm tra bằng docker exec -it <id> ls /app/data.

15

Tóm tắt

  • ✅ Docker giải quyết dependency hell cho AI project bằng cách đóng gói môi trường vào image
  • ✅ Image = snapshot read-only; Container = instance đang chạy; Dockerfile = recipe để build image
  • ✅ Cài Docker Desktop (macOS/Windows) hoặc Docker Engine (Linux); verify bằng docker run hello-world
  • ✅ Các lệnh hay dùng: run, build, ps, stop, rm, exec, logs
  • -p để forward port, -v để mount volume, -e để set env variable
  • ✅ Sắp xếp instruction Dockerfile từ ít thay đổi đến hay thay đổi nhất để tận dụng layer cache
  • .dockerignore loại dataset, model weight, .env, .git/ ra khỏi image
  • ✅ Chọn base image theo nhu cầu: python:3.11-slim cho CPU, pytorch/pytorch:2.3.0-cuda12.1-cudnn8-runtime cho GPU
  • ✅ Không dùng latest tag; không chạy container root; luôn --no-cache-dir với pip
16

Bài tiếp theo

Bài 33: Dockerfile cho ứng dụng AI Python — best practices — đi sâu hơn vào các pattern thực tế: cài system lib cho ML, xử lý CUDA dependency, non-root user, health check, và các cải tiến giúp image production-ready.