- Published on
Day 43: Docker/K8s/GPU Serving Cho AI Workload
- Authors

- Name
- Trần Mạnh Thắng
- @TranManhThang96
1. Mục tiêu bài học
Day 43 tập trung vào deployment layer cho AI system. Sau Day 40-42, bạn đã có RAG/LLM service, streaming API và các lựa chọn serving như managed LLM, vLLM hoặc TGI. Bài này trả lời câu hỏi thực tế hơn:
Làm sao đóng gói service đó thành artifact repeatable?
Làm sao reviewer chạy được bằng Docker Compose?
Nếu lên Kubernetes thì cần manifest nào?
Nếu workload cần GPU thì scheduler biết node nào có GPU bằng cách nào?
Nếu đưa vào production thì còn thiếu điều kiện gì?
Mục tiêu không phải học toàn bộ Kubernetes. Mục tiêu là biết đủ để deploy một AI workload có trách nhiệm:
- Docker image nhỏ, reproducible, không chứa secret.
- Runtime config đi qua environment variable hoặc secret store.
- Health/readiness endpoint phản ánh đúng trạng thái model, vector DB và dependency.
- Docker Compose chạy được local stack bằng một lệnh.
- Kubernetes manifests có resource boundary, probes, rollout strategy và secret/config separation.
- GPU pod chỉ chạy trên GPU node, request đúng
nvidia.com/gpuvà không chiếm GPU node cho workload thường. - Có trade-off rõ giữa Compose, Kubernetes, Helm, KServe, Ray Serve, managed LLM và self-host GPU.
2. Mental model
AI service production không chỉ là:
FastAPI app -> docker build -> docker run
Một deployment tốt phải quản lý cả artifact, runtime state và operational behavior:
Source code
-> locked dependencies
-> Docker image
-> config/secrets
-> runtime volumes
-> health/readiness
-> scheduler constraints
-> rollout/rollback
-> logs/metrics/traces
-> smoke test
Với AI workload, bạn có thêm vài constraint không thường gặp ở backend CRUD:
- Model loading có thể mất vài chục giây đến vài phút.
- VRAM là tài nguyên giới hạn hơn CPU/RAM.
- Context length và concurrency làm KV cache tăng nhanh.
- Cold start có thể làm readiness sai nếu chỉ kiểm tra process alive.
- GPU driver, CUDA runtime, PyTorch/TensorRT/vLLM version phải tương thích.
- Vector DB và index version cần backup, migration và rollback plan.
- Streaming response cần timeout, graceful shutdown và client disconnect handling.
3. Target architecture cho bài học
Phiên bản local bằng Docker Compose:
Browser / curl
-> api: FastAPI RAG service
-> qdrant: vector DB
-> optional local model server
-> optional managed LLM API
-> volumes: document data, vector DB data, model cache
Phiên bản Kubernetes:
Ingress / Gateway
-> Service api
-> Deployment api
-> ConfigMap: non-secret config
-> Secret: API keys
-> PVC: optional data/cache
-> Service vector-db
-> StatefulSet vector-db or managed vector DB
-> Service model-server
-> Deployment model-server on GPU nodes
Best practice trong nhiều team là tách API orchestration và model serving:
api: authentication, request validation, RAG orchestration, prompt policy, citations, tracing.vector-db: Qdrant, pgvector, Milvus hoặc managed vector DB.model-server: vLLM/TGI/Triton/Ollama hoặc managed LLM provider.observability: logs, metrics, traces, dashboards, alerts.
Tách như vậy giúp scale API và GPU inference độc lập. API thường scale theo request count; GPU model server scale theo tokens/sec, queue depth, VRAM và p95 latency.
4. Docker image cho ML/AI
4.1 Nguyên tắc thiết kế image
Một Docker image dùng cho AI backend nên đạt các tiêu chí:
- Pin base image và Python version.
- Lock dependency bằng
requirements.lock,uv.lock,poetry.lockhoặc equivalent. - Tách layer dependency và layer source code để tận dụng build cache.
- Không copy
.env, API key, dataset thô, vector index, model cache lớn hoặc notebook rác vào image. - Chạy bằng non-root user nếu service không cần privilege.
- Log ra stdout/stderr.
- Có
HEALTHCHECKhoặc ít nhất app có/healthvà/ready. - Dùng image tag immutable hoặc digest khi deploy production.
- Không dùng
latesttrong production manifest.
.dockerignore quan trọng không kém Dockerfile:
.git
.venv
__pycache__
.pytest_cache
.mypy_cache
.ruff_cache
.env
.env.*
!.env.example
data/raw
data/uploads
data/vector_store
models
model_cache
reports
*.sqlite
*.db
*.parquet
*.pt
*.safetensors
Không ignore .env.example vì reviewer cần biết config nào phải khai báo.
4.2 Dockerfile CPU cho FastAPI RAG backend
Ví dụ này phù hợp với RAG API dùng managed LLM hoặc embedding API bên ngoài. Nó không cần GPU trong container.
# syntax=docker/dockerfile:1.7
FROM python:3.11-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
APP_HOME=/app
WORKDIR ${APP_HOME}
RUN groupadd --system app && useradd --system --gid app --home-dir ${APP_HOME} app
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.lock ./requirements.lock
RUN \
pip install --upgrade pip \
&& pip install -r requirements.lock
COPY app ./app
COPY pyproject.toml README.md ./
RUN chown -R app:app ${APP_HOME}
USER app
EXPOSE 8000
HEALTHCHECK \
CMD curl -fsS http://127.0.0.1:8000/health || exit 1
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"]
Điểm gần production:
- Có non-root user.
- Có lock file thay vì dependency trôi nổi.
- Cài dependency trước khi copy source code.
- Có healthcheck.
- Không copy toàn bộ repo một cách mù quáng.
- Không bake secret vào image.
Nếu app cần package native như psycopg, pymupdf, torch, faiss, bạn có thể cần stage build riêng hoặc base image có đủ system library. Không chọn Alpine cho ML Python nếu dependency native làm build phức tạp; python:slim thường thực dụng hơn.
4.3 Dockerfile GPU cho local model/reranker service
GPU image chỉ cần khi container trực tiếp chạy model bằng CUDA. Nếu API chỉ gọi OpenAI/Anthropic/Azure hoặc gọi model server khác qua HTTP, API image không cần NVIDIA base image.
Ví dụ skeleton:
# syntax=docker/dockerfile:1.7
ARG CUDA_IMAGE=nvidia/cuda:12.4.1-cudnn-runtime-ubuntu22.04
FROM ${CUDA_IMAGE} AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
APP_HOME=/app \
HF_HOME=/models/huggingface \
TRANSFORMERS_CACHE=/models/huggingface
WORKDIR ${APP_HOME}
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 python3-pip curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN groupadd --system app && useradd --system --gid app --home-dir ${APP_HOME} app
COPY requirements-gpu.lock ./requirements-gpu.lock
RUN \
python3 -m pip install --upgrade pip \
&& python3 -m pip install -r requirements-gpu.lock
COPY app ./app
RUN mkdir -p /models/huggingface && chown -R app:app ${APP_HOME} /models
USER app
EXPOSE 8001
HEALTHCHECK \
CMD curl -fsS http://127.0.0.1:8001/health || exit 1
CMD ["python3", "-m", "app.model_server"]
Lưu ý quan trọng:
- CUDA image version phải tương thích với framework wheels bạn cài.
- Host driver phải đủ mới cho CUDA runtime trong container.
- Model cache nên mount volume/PVC, không copy model hàng chục GB vào image trừ khi bạn có lý do rõ.
start-periodcủa healthcheck GPU thường dài hơn vì model load lâu.- Không chạy container GPU với
--privilegedchỉ để thấy GPU. Cài đúng NVIDIA runtime/toolkit.
5. .env.example
.env.example là contract giữa code và runtime. Nó phải đủ rõ để reviewer chạy được mà không đọc toàn bộ source.
# App
APP_ENV=local
APP_NAME=rag-serving
LOG_LEVEL=INFO
PORT=8000
WORKERS=1
REQUEST_TIMEOUT_SECONDS=60
MAX_UPLOAD_MB=25
MAX_CONCURRENT_REQUESTS=16
# LLM mode: managed | local
LLM_MODE=managed
LLM_PROVIDER=openai
LLM_MODEL=gpt-4.1-mini
OPENAI_API_KEY=replace-me
# Local model server, used when LLM_MODE=local
MODEL_SERVER_URL=http://model-server:8001/v1
MODEL_ID=meta-llama/Llama-3.1-8B-Instruct
MODEL_CACHE_DIR=/models/huggingface
# Embedding
EMBEDDING_PROVIDER=openai
EMBEDDING_MODEL=text-embedding-3-small
EMBEDDING_DIM=1536
# Vector DB
VECTOR_DB=qdrant
QDRANT_URL=http://qdrant:6333
QDRANT_COLLECTION=policy_chunks
# Index/prompt versioning
INDEX_VERSION=rag-v1
PROMPT_VERSION=answer-v1
CHUNKING_VERSION=chunk-v1
# Observability
OTEL_EXPORTER_OTLP_ENDPOINT=
TRACE_SAMPLE_RATE=1.0
Production note:
.env.exampleđược commit..envkhông được commit.- Secret thật nên nằm trong secret manager, external secret controller hoặc CI/CD secret store.
- Với Kubernetes, non-secret config vào
ConfigMap, secret vàoSecrethoặc external secret.
6. Docker Compose cho RAG stack
Compose dùng để local demo, integration test và portfolio review. Compose không thay thế Kubernetes production, nhưng nó là deliverable bắt buộc vì giúp người khác chạy hệ thống nhanh.
Ví dụ Compose gần production hơn bản tối giản:
name: rag-serving
services:
api:
build:
context: ./backend
dockerfile: Dockerfile
image: rag-serving-api:${APP_IMAGE_TAG:-local}
env_file:
- .env
environment:
QDRANT_URL: http://qdrant:6333
ports:
- "8000:8000"
depends_on:
qdrant:
condition: service_started
volumes:
- ./data/sample_docs:/app/data/sample_docs:ro
- ./reports:/app/reports
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
restart: unless-stopped
qdrant:
image: qdrant/qdrant:${QDRANT_TAG:-v1.14.1}
ports:
- "6333:6333"
volumes:
- qdrant_data:/qdrant/storage
restart: unless-stopped
model-server:
profiles: ["gpu"]
build:
context: ./model-server
dockerfile: Dockerfile.gpu
image: rag-model-server:${MODEL_IMAGE_TAG:-local}
env_file:
- .env
ports:
- "8001:8001"
volumes:
- model_cache:/models/huggingface
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: ["gpu"]
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8001/health"]
interval: 30s
timeout: 5s
retries: 10
start_period: 120s
restart: unless-stopped
volumes:
qdrant_data:
model_cache:
Qdrant có các endpoint /healthz, /livez, /readyz, nhưng Docker healthcheck chạy bên trong container. Nếu image không có curl, wget hoặc binary healthcheck riêng, healthcheck trong Compose sẽ giòn. Cách thực dụng cho lab là để API /ready retry và báo dependency chưa sẵn sàng; với Kubernetes, dùng httpGet probe trực tiếp từ kubelet.
Chạy CPU path:
cp .env.example .env
docker compose up --build
curl -fsS http://localhost:8000/health
curl -fsS http://localhost:8000/ready
Chạy GPU profile:
docker compose --profile gpu up --build
Trade-off của Compose:
- Mạnh: dễ chạy local, dễ review, ít dependency.
- Yếu: không đại diện đầy đủ cho scheduling, autoscaling, rollout, secret management, network policy.
- Best use: portfolio, integration test, demo environment, single-VM internal tool nhỏ.
7. Healthcheck và readiness cho AI service
Tối thiểu nên có:
GET /health
-> process còn sống
-> event loop phản hồi
-> không gọi dependency nặng
GET /ready
-> app config hợp lệ
-> vector DB reachable
-> collection/index tồn tại
-> model provider reachable hoặc local model loaded
-> migration/index version đúng
Ví dụ FastAPI:
from fastapi import APIRouter, status
from fastapi.responses import JSONResponse
router = APIRouter()
@router.get("/health")
async def health() -> dict[str, str]:
return {"status": "ok"}
@router.get("/ready")
async def ready() -> JSONResponse:
checks = {
"config": True,
"vector_db": await check_qdrant(),
"llm": await check_llm_provider(),
"index_version": await check_index_version(),
}
http_status = status.HTTP_200_OK if all(checks.values()) else status.HTTP_503_SERVICE_UNAVAILABLE
return JSONResponse({"status": "ready" if http_status == 200 else "not_ready", "checks": checks}, status_code=http_status)
Không nên để /ready gọi một LLM generation đầy đủ cho mỗi probe; chi phí và latency sẽ tệ. Với managed LLM, check nhẹ bằng config/token validation hoặc endpoint metadata nếu provider hỗ trợ. Với local model, check trạng thái model loaded hoặc warmup marker nội bộ.
8. NVIDIA container stack
Để container dùng được NVIDIA GPU, cần phân biệt bốn lớp:
| Lớp | Vai trò |
|---|---|
| Host NVIDIA driver | Driver thật trên node/VM, giao tiếp với GPU |
| NVIDIA Container Toolkit | Cho container runtime expose GPU device/library vào container |
| Container runtime | Docker/containerd/CRI-O được cấu hình runtime NVIDIA khi cần |
| Kubernetes device plugin | Advertise GPU thành schedulable resource như nvidia.com/gpu |
Local Docker smoke test:
docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi
Kubernetes GPU node checklist:
nvidia-smi
kubectl get nodes
kubectl describe node <gpu-node> | grep -A5 "nvidia.com/gpu"
kubectl get pods -n nvidia-device-plugin
Theo Kubernetes, GPU được expose qua device plugin như custom schedulable resource. Với NVIDIA, resource phổ biến là:
resources:
limits:
nvidia.com/gpu: 1
Điểm dễ sai:
- GPU phải nằm trong
limits. Nếu khai báo cảrequestsvàlimitsthì hai giá trị phải bằng nhau. - Pod không request
nvidia.com/gputhì scheduler không reserve GPU cho pod. - Taint chỉ chặn pod không phù hợp;
nodeSelector/node affinity mới kéo pod về đúng GPU node. - Nếu GPU node bị taint,
nvidia-device-pluginDaemonSet cũng cần toleration phù hợp để chạy trên node đó. - Không assume một GPU request luôn là độc quyền nếu cluster bật MIG, MPS hoặc time-slicing. Cần hiểu policy của cluster.
9. Kubernetes scheduling cho GPU
9.1 Label GPU node
Ví dụ label node có NVIDIA L4:
kubectl label node gpu-node-1 accelerator=nvidia-l4
kubectl label node gpu-node-1 workload=ai-inference
9.2 Taint GPU node
Mục tiêu là tránh workload thường chạy lên GPU node:
kubectl taint node gpu-node-1 nvidia.com/gpu=true:NoSchedule
9.3 Pod cần cả selector và toleration
nodeSelector:
accelerator: nvidia-l4
workload: ai-inference
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
containers:
- name: model-server
resources:
limits:
nvidia.com/gpu: 1
Ý nghĩa:
nodeSelector: chỉ chọn node có label phù hợp.tolerations: cho phép pod chạy trên node có taint tương ứng.nvidia.com/gpu: scheduler reserve GPU cho container.
Nếu chỉ có toleration mà không có selector, pod "được phép" chạy trên GPU node nhưng không bị bắt buộc chạy ở đó. Nếu chỉ có selector mà node có taint, pod vẫn không schedule được.
10. Kubernetes manifests gần production
10.1 ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: rag-api-config
data:
APP_ENV: "production"
LOG_LEVEL: "INFO"
LLM_MODE: "managed"
LLM_PROVIDER: "openai"
LLM_MODEL: "gpt-4.1-mini"
EMBEDDING_MODEL: "text-embedding-3-small"
VECTOR_DB: "qdrant"
QDRANT_URL: "http://qdrant:6333"
QDRANT_COLLECTION: "policy_chunks"
INDEX_VERSION: "rag-v1"
PROMPT_VERSION: "answer-v1"
REQUEST_TIMEOUT_SECONDS: "60"
MAX_CONCURRENT_REQUESTS: "32"
10.2 Secret
apiVersion: v1
kind: Secret
metadata:
name: rag-api-secret
type: Opaque
stringData:
OPENAI_API_KEY: "replace-in-secret-manager"
Production không nên commit secret manifest chứa giá trị thật. Dùng External Secrets, Sealed Secrets, SOPS hoặc secret manager của cloud provider.
10.3 Deployment API
apiVersion: apps/v1
kind: Deployment
metadata:
name: rag-api
labels:
app: rag-api
spec:
replicas: 2
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: rag-api
template:
metadata:
labels:
app: rag-api
spec:
terminationGracePeriodSeconds: 60
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: api
image: ghcr.io/acme/rag-api:2026.05.10-abcdef
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8000
name: http
envFrom:
- configMapRef:
name: rag-api-config
- secretRef:
name: rag-api-secret
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 6
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 3
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
10.4 Service
apiVersion: v1
kind: Service
metadata:
name: rag-api
spec:
type: ClusterIP
selector:
app: rag-api
ports:
- name: http
port: 8000
targetPort: http
10.5 GPU model server Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: model-server
labels:
app: model-server
spec:
replicas: 1
selector:
matchLabels:
app: model-server
template:
metadata:
labels:
app: model-server
spec:
terminationGracePeriodSeconds: 120
nodeSelector:
accelerator: nvidia-l4
workload: ai-inference
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
containers:
- name: model-server
image: ghcr.io/acme/rag-model-server:2026.05.10-abcdef
ports:
- containerPort: 8001
name: http
env:
- name: MODEL_ID
value: "meta-llama/Llama-3.1-8B-Instruct"
- name: MODEL_CACHE_DIR
value: "/models/huggingface"
resources:
requests:
cpu: "2"
memory: "12Gi"
limits:
cpu: "8"
memory: "24Gi"
nvidia.com/gpu: 1
volumeMounts:
- name: model-cache
mountPath: /models/huggingface
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 120
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 20
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 180
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 5
volumes:
- name: model-cache
persistentVolumeClaim:
claimName: model-cache-pvc
GPU production note:
replicas: 1là mặc định an toàn cho một model server khi bạn chưa có load test.- Scale GPU theo queue depth, tokens/sec, p95 latency và VRAM, không chỉ CPU.
- Nếu model load lâu, dùng readiness delay dài, rolling strategy cẩn thận và warmup trước khi nhận traffic.
- Với multi-GPU hoặc tensor parallel, manifest cần thêm logic riêng của serving engine.
11. Helm overview
Helm là package manager cho Kubernetes. Với AI workload, Helm hữu ích khi bạn có nhiều manifest lặp lại theo environment:
charts/rag-serving/
Chart.yaml
values.yaml
templates/
deployment.yaml
service.yaml
configmap.yaml
secret.yaml
hpa.yaml
Khi nên dùng Helm:
- Có nhiều environment: dev, staging, prod.
- Cần parameterize image tag, replica, resource, node selector, secret reference.
- Team đã có GitOps/Helm workflow.
- Muốn reuse chart cho nhiều model/service.
Khi chưa nên dùng Helm:
- Bạn mới học Kubernetes và manifest còn ít.
- Team chưa có release workflow rõ.
- Chart chỉ bọc lại 2-3 file YAML nhưng làm debug khó hơn.
Best path cho bài học:
- Viết raw manifests trước để hiểu object.
- Sau khi manifest ổn, mới chuyển sang Helm chart.
- Không dùng Helm để che lấp việc chưa hiểu scheduling, probes và resource.
12. KServe overview
KServe là một inference platform trên Kubernetes cho predictive và generative inference. Nó cung cấp abstraction như InferenceService, model runtime, autoscaling, protocol chuẩn và integration với model serving runtimes.
Khi KServe phù hợp:
- Platform team đã vận hành Kubernetes tốt.
- Có nhiều model cần deploy theo pattern giống nhau.
- Cần standardized inference endpoint, autoscaling, canary/rollout, model runtime.
- Muốn tách model serving platform khỏi business API.
Khi KServe có thể quá nặng:
- Chỉ có một RAG app nhỏ.
- Team chưa vận hành Kubernetes/GPU ổn định.
- Logic chính nằm ở orchestration/RAG pipeline hơn là pure model inference.
Mental model:
Client/API
-> InferenceService
-> predictor runtime
-> model storage/cache
-> autoscaling
-> standardized inference protocol
Với RAG, thường không đưa toàn bộ RAG orchestration vào KServe. Cách sạch hơn:
- FastAPI vẫn làm RAG orchestration.
- KServe phục vụ embedding/reranker/local LLM như model endpoint.
- API gọi KServe endpoint qua HTTP/gRPC.
13. Ray Serve overview
Ray Serve là framework serving online inference chạy trên Ray. Nó hợp với model composition và pipeline nhiều bước bằng Python:
HTTP request
-> Ray Serve deployment A: preprocess
-> deployment B: retriever/reranker
-> deployment C: model inference
-> deployment D: postprocess
Khi Ray Serve phù hợp:
- Pipeline inference nhiều model, nhiều bước, cần composition rõ.
- Cần dynamic batching, streaming, multi-node hoặc multi-GPU scheduling.
- Team đã dùng Ray cho batch/distributed workload.
- Muốn scale từng deployment trong pipeline độc lập.
Khi Ray Serve có thể quá nặng:
- RAG API đơn giản, traffic thấp.
- Team chưa có kinh nghiệm vận hành Ray cluster.
- Managed LLM đã đáp ứng latency/cost.
Rule thực dụng:
- FastAPI + managed LLM: tốt cho MVP và nhiều production app vừa/nhỏ.
- FastAPI + vLLM/TGI trên GPU: tốt khi cần self-host LLM rõ ràng.
- KServe: tốt khi platform team chuẩn hóa model serving trên Kubernetes.
- Ray Serve: tốt khi inference graph phức tạp và cần scale nhiều bước bằng Ray.
14. Trade-off matrix
| Lựa chọn | Điểm mạnh | Điểm yếu | Khi chọn |
|---|---|---|---|
| Docker Compose | Chạy local nhanh, dễ review | Không có scheduling/rollout thật | Portfolio, demo, integration test |
| Raw K8s manifests | Rõ object, ít magic | Lặp YAML, khó quản lý nhiều env | Học, service ít, platform nhỏ |
| Helm | Template hóa release | Debug chart/values phức tạp | Nhiều env/service, GitOps |
| Managed LLM | Ít ops GPU, ship nhanh | Cost, privacy, rate limit, vendor dependency | MVP, business app, traffic vừa |
| Self-host GPU | Control model/data/latency | Cần GPU ops, capacity planning | Privacy cao, volume lớn, custom model |
| KServe | Chuẩn hóa model serving | Platform overhead | Nhiều model trên K8s |
| Ray Serve | Composition, batching, distributed | Vận hành Ray cluster | Multi-model pipeline phức tạp |
| Image chứa model | Startup nhanh hơn | Image rất lớn, rollback chậm | Edge/offline, model nhỏ, release ít |
| Mount model cache | Image nhỏ, update dễ | Cold start tải model | Dev/staging, model lớn, cache tốt |
| Scale API replicas | Rẻ và dễ | Dependency/shared state cần thiết kế | API stateless |
| Scale GPU replicas | Tăng throughput | Tốn GPU, warmup lâu | Traffic cao, queue depth cao |
15. Best solution theo context/performance
Không có một deployment strategy tốt nhất cho mọi AI workload. Dưới đây là lựa chọn thực dụng.
| Context | Best solution | Lý do |
|---|---|---|
| Capstone/portfolio | Docker Compose + CPU API + Qdrant + managed LLM | Reviewer chạy nhanh, ít yêu cầu phần cứng |
| Internal demo trên một VM | Docker Compose hoặc systemd + Docker, managed LLM | Đơn giản, đủ kiểm soát, chi phí ops thấp |
| Production nhỏ, traffic vừa | Kubernetes API stateless + managed vector DB + managed LLM | Tập trung vào reliability/security hơn GPU ops |
| Privacy cao, không gửi dữ liệu ra ngoài | Kubernetes + self-host embedding/reranker/LLM trên GPU | Control data egress, cần benchmark và hardening |
| Throughput LLM cao | Dedicated model server với vLLM/TGI + queue/batching + GPU autoscaling policy | Tối ưu tokens/sec và VRAM |
| Nhiều model/team cùng deploy | KServe + Helm/GitOps + shared observability | Chuẩn hóa platform, giảm drift |
| Inference pipeline nhiều bước | Ray Serve hoặc service composition rõ | Scale từng stage, batching và routing tốt hơn |
Với bài Day 43, best baseline là:
Local/portfolio:
Docker Compose
FastAPI API
Qdrant
managed LLM by default
optional GPU model-server profile
Kubernetes optional:
Deployment + Service + ConfigMap + Secret
resource requests/limits
readiness/liveness probes
GPU model-server manifest with nodeSelector/tolerations/nvidia.com/gpu
16. Production readiness answer
Dùng được trong production không?
Có, nhưng không phải chỉ với file Dockerfile và Compose của bài học. Bộ cấu hình trong bài này là production-oriented baseline, chưa phải production hoàn chỉnh cho mọi doanh nghiệp.
Nếu có thì cần điều kiện gì?
Cần tối thiểu các điều kiện sau:
- Image được build trong CI, dependency locked, vulnerability scan, tag immutable hoặc digest.
- Secret không nằm trong image, Git repo hoặc plain Kubernetes Secret không mã hóa ở rest ngoài chuẩn cluster.
/healthvà/readyphản ánh đúng trạng thái app, vector DB, model provider và index version.- API stateless hoặc state được đưa vào DB/object storage/vector DB có backup.
- Resource requests/limits được sizing bằng benchmark thật.
- GPU node có driver, NVIDIA Container Toolkit, device plugin và monitoring đúng.
- Rollout có smoke test, rollback plan cho image, prompt version, index version và model version.
- Observability có structured logs, metrics p50/p95/p99 latency, error rate, token usage, cost và trace ID.
- Security có authn/authz, tenant/ACL filter server-side, network policy, request size limit và rate limit.
- Data có backup/restore test cho vector DB, metadata DB và document store.
- Load test chứng minh p95 latency, throughput, VRAM và cost nằm trong SLO.
- Có incident runbook: provider outage, vector DB down, GPU OOM, model server cold start, bad index release.
Nếu thiếu các điều kiện trên, hệ thống vẫn có thể dùng cho demo, staging hoặc internal prototype, nhưng chưa nên gọi là production-ready.
17. Checklist cuối bài
- Có
Dockerfilecho API. - Có
.dockerignore. - Có
.env.exampleđầy đủ. - Có
docker-compose.ymlchạy được CPU path. - Có optional GPU service/profile nếu dùng local model.
- Có
/healthvà/ready. - Có smoke test script.
- Có K8s
Deployment,Service,ConfigMap,Secret. - Có GPU manifest dùng
nodeSelector, tolerations vànvidia.com/gpu. - Có resource estimate CPU/RAM/VRAM.
- Có trade-off và best solution theo context.
- README/deployment note trả lời production readiness.
18. Nguồn tham khảo chính thức
- Docker build best practices: https://docs.docker.com/build/building/best-practices/
- Docker Compose service reference: https://docs.docker.com/reference/compose-file/services/
- Qdrant monitoring endpoints: https://qdrant.tech/documentation/ops-monitoring/monitoring/
- Kubernetes GPU scheduling: https://kubernetes.io/docs/tasks/manage-gpus/scheduling-gpus/
- Kubernetes node selection: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/
- Kubernetes taints/tolerations: https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/
- NVIDIA Kubernetes device plugin: https://github.com/NVIDIA/k8s-device-plugin
- KServe overview: https://kserve.github.io/kserve/
- Ray Serve overview: https://docs.ray.io/en/latest/serve/
Tài liệu
Tài liệu này gom các template thực dụng để bạn dùng khi làm lab Day 43 hoặc nâng cấp capstone Day 40. Hãy xem đây là starting point, không phải cấu hình production universal.
1. Project structure đề xuất
rag-serving/
backend/
app/
main.py
settings.py
health.py
rag_pipeline.py
tests/
Dockerfile
requirements.in
requirements.lock
pyproject.toml
model-server/
app/
model_server.py
Dockerfile.gpu
requirements-gpu.lock
k8s/
namespace.yaml
configmap.yaml
secret.example.yaml
api-deployment.yaml
api-service.yaml
qdrant-statefulset.yaml
qdrant-service.yaml
model-server-deployment.yaml
model-server-service.yaml
pdb.yaml
scripts/
smoke-test.sh
benchmark.sh
data/
sample_docs/
reports/
.dockerignore
.env.example
docker-compose.yml
README.md
Rule ownership:
- Source code vào image.
- Runtime config vào env/config map/secret.
- Runtime data vào volume/PVC/object storage.
- Model cache lớn vào volume/PVC, không bake vào image mặc định.
2. Dockerfile template cho API
# syntax=docker/dockerfile:1.7
FROM python:3.11-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
APP_HOME=/app
WORKDIR ${APP_HOME}
RUN groupadd --system app && useradd --system --gid app --home-dir ${APP_HOME} app
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.lock ./requirements.lock
RUN \
pip install --upgrade pip \
&& pip install -r requirements.lock
COPY app ./app
COPY pyproject.toml README.md ./
RUN chown -R app:app ${APP_HOME}
USER app
EXPOSE 8000
HEALTHCHECK \
CMD curl -fsS http://127.0.0.1:8000/health || exit 1
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"]
3. Dockerfile.gpu template
# syntax=docker/dockerfile:1.7
ARG CUDA_IMAGE=nvidia/cuda:12.4.1-cudnn-runtime-ubuntu22.04
FROM ${CUDA_IMAGE} AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
APP_HOME=/app \
HF_HOME=/models/huggingface \
TRANSFORMERS_CACHE=/models/huggingface
WORKDIR ${APP_HOME}
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 python3-pip curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN groupadd --system app && useradd --system --gid app --home-dir ${APP_HOME} app
COPY requirements-gpu.lock ./requirements-gpu.lock
RUN \
python3 -m pip install --upgrade pip \
&& python3 -m pip install -r requirements-gpu.lock
COPY app ./app
RUN mkdir -p /models/huggingface && chown -R app:app ${APP_HOME} /models
USER app
EXPOSE 8001
HEALTHCHECK \
CMD curl -fsS http://127.0.0.1:8001/health || exit 1
CMD ["python3", "-m", "app.model_server"]
4. .env.example template
APP_ENV=local
APP_NAME=rag-serving
LOG_LEVEL=INFO
PORT=8000
WORKERS=1
REQUEST_TIMEOUT_SECONDS=60
MAX_UPLOAD_MB=25
MAX_CONCURRENT_REQUESTS=16
LLM_MODE=managed
LLM_PROVIDER=openai
LLM_MODEL=gpt-4.1-mini
OPENAI_API_KEY=replace-me
MODEL_SERVER_URL=http://model-server:8001/v1
MODEL_ID=meta-llama/Llama-3.1-8B-Instruct
MODEL_CACHE_DIR=/models/huggingface
EMBEDDING_PROVIDER=openai
EMBEDDING_MODEL=text-embedding-3-small
EMBEDDING_DIM=1536
VECTOR_DB=qdrant
QDRANT_URL=http://qdrant:6333
QDRANT_COLLECTION=policy_chunks
INDEX_VERSION=rag-v1
PROMPT_VERSION=answer-v1
CHUNKING_VERSION=chunk-v1
OTEL_EXPORTER_OTLP_ENDPOINT=
TRACE_SAMPLE_RATE=1.0
APP_IMAGE_TAG=local
MODEL_IMAGE_TAG=local
QDRANT_TAG=v1.14.1
5. Docker Compose template
name: rag-serving
services:
api:
build:
context: ./backend
dockerfile: Dockerfile
image: rag-serving-api:${APP_IMAGE_TAG:-local}
env_file:
- .env
environment:
QDRANT_URL: http://qdrant:6333
ports:
- "8000:8000"
depends_on:
qdrant:
condition: service_started
volumes:
- ./data/sample_docs:/app/data/sample_docs:ro
- ./reports:/app/reports
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
restart: unless-stopped
qdrant:
image: qdrant/qdrant:${QDRANT_TAG:-v1.14.1}
ports:
- "6333:6333"
volumes:
- qdrant_data:/qdrant/storage
restart: unless-stopped
model-server:
profiles: ["gpu"]
build:
context: ./model-server
dockerfile: Dockerfile.gpu
image: rag-model-server:${MODEL_IMAGE_TAG:-local}
env_file:
- .env
ports:
- "8001:8001"
volumes:
- model_cache:/models/huggingface
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: ["gpu"]
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8001/health"]
interval: 30s
timeout: 5s
retries: 10
start_period: 120s
restart: unless-stopped
volumes:
qdrant_data:
model_cache:
Qdrant expose /healthz, /livez và /readyz, nhưng Compose healthcheck chạy bên trong container. Nếu image không có curl, wget hoặc healthcheck binary, đừng thêm healthcheck giòn; hãy để API /ready retry/report trạng thái Qdrant. Trong Kubernetes, httpGet probe không cần tool bên trong container.
6. Kubernetes namespace
apiVersion: v1
kind: Namespace
metadata:
name: rag-serving
labels:
name: rag-serving
7. Kubernetes ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: rag-api-config
namespace: rag-serving
data:
APP_ENV: "production"
APP_NAME: "rag-serving"
LOG_LEVEL: "INFO"
PORT: "8000"
WORKERS: "2"
REQUEST_TIMEOUT_SECONDS: "60"
MAX_UPLOAD_MB: "25"
MAX_CONCURRENT_REQUESTS: "32"
LLM_MODE: "managed"
LLM_PROVIDER: "openai"
LLM_MODEL: "gpt-4.1-mini"
EMBEDDING_MODEL: "text-embedding-3-small"
EMBEDDING_DIM: "1536"
VECTOR_DB: "qdrant"
QDRANT_URL: "http://qdrant.rag-serving.svc.cluster.local:6333"
QDRANT_COLLECTION: "policy_chunks"
INDEX_VERSION: "rag-v1"
PROMPT_VERSION: "answer-v1"
CHUNKING_VERSION: "chunk-v1"
8. Kubernetes Secret example
apiVersion: v1
kind: Secret
metadata:
name: rag-api-secret
namespace: rag-serving
type: Opaque
stringData:
OPENAI_API_KEY: "replace-in-secret-manager"
Không dùng file trên với secret thật. Trong production, ưu tiên External Secrets, Sealed Secrets, SOPS hoặc secret manager của cloud provider.
9. API Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: rag-api
namespace: rag-serving
labels:
app: rag-api
spec:
replicas: 2
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: rag-api
template:
metadata:
labels:
app: rag-api
spec:
terminationGracePeriodSeconds: 60
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: api
image: ghcr.io/acme/rag-api:2026.05.10-abcdef
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8000
envFrom:
- configMapRef:
name: rag-api-config
- secretRef:
name: rag-api-secret
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 6
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 3
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
10. API Service
apiVersion: v1
kind: Service
metadata:
name: rag-api
namespace: rag-serving
spec:
type: ClusterIP
selector:
app: rag-api
ports:
- name: http
port: 8000
targetPort: http
11. Qdrant StatefulSet cho lab
Production lớn nên cân nhắc Qdrant chart/operator/managed service và backup rõ ràng. Template dưới đây chỉ đủ cho lab hoặc namespace nhỏ.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: qdrant
namespace: rag-serving
spec:
serviceName: qdrant
replicas: 1
selector:
matchLabels:
app: qdrant
template:
metadata:
labels:
app: qdrant
spec:
containers:
- name: qdrant
image: qdrant/qdrant:v1.14.1
ports:
- name: http
containerPort: 6333
- name: grpc
containerPort: 6334
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
readinessProbe:
httpGet:
path: /readyz
port: http
initialDelaySeconds: 10
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 30
periodSeconds: 30
volumeMounts:
- name: qdrant-data
mountPath: /qdrant/storage
volumeClaimTemplates:
- metadata:
name: qdrant-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 20Gi
12. Qdrant Service
apiVersion: v1
kind: Service
metadata:
name: qdrant
namespace: rag-serving
spec:
type: ClusterIP
selector:
app: qdrant
ports:
- name: http
port: 6333
targetPort: http
- name: grpc
port: 6334
targetPort: grpc
13. GPU model server PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: model-cache-pvc
namespace: rag-serving
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 100Gi
14. GPU model server Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: model-server
namespace: rag-serving
labels:
app: model-server
spec:
replicas: 1
selector:
matchLabels:
app: model-server
template:
metadata:
labels:
app: model-server
spec:
terminationGracePeriodSeconds: 120
nodeSelector:
accelerator: nvidia-l4
workload: ai-inference
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
containers:
- name: model-server
image: ghcr.io/acme/rag-model-server:2026.05.10-abcdef
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8001
env:
- name: MODEL_ID
value: "meta-llama/Llama-3.1-8B-Instruct"
- name: MODEL_CACHE_DIR
value: "/models/huggingface"
resources:
requests:
cpu: "2"
memory: "12Gi"
limits:
cpu: "8"
memory: "24Gi"
nvidia.com/gpu: 1
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 120
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 20
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 180
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 5
volumeMounts:
- name: model-cache
mountPath: /models/huggingface
volumes:
- name: model-cache
persistentVolumeClaim:
claimName: model-cache-pvc
15. GPU model server Service
apiVersion: v1
kind: Service
metadata:
name: model-server
namespace: rag-serving
spec:
type: ClusterIP
selector:
app: model-server
ports:
- name: http
port: 8001
targetPort: http
16. PodDisruptionBudget cho API
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: rag-api-pdb
namespace: rag-serving
spec:
minAvailable: 1
selector:
matchLabels:
app: rag-api
Với GPU model server chỉ có một replica, PDB không tạo high availability thật. HA cho model server cần nhiều GPU replica, traffic routing và capacity đủ lớn.
17. Cài NVIDIA device plugin bằng Helm
Checklist trước:
nvidia-smi
containerd --version
kubectl get nodes
Ví dụ cài plugin:
helm repo add nvdp https://nvidia.github.io/k8s-device-plugin
helm repo update
helm upgrade -i nvdp nvdp/nvidia-device-plugin \
--namespace nvidia-device-plugin \
--create-namespace \
--version 0.17.1
Kiểm chứng:
kubectl get pods -n nvidia-device-plugin
kubectl describe node <gpu-node> | grep -A5 "nvidia.com/gpu"
Nếu GPU node có taint riêng, đảm bảo DaemonSet của device plugin có toleration tương ứng. Nếu không, plugin không chạy trên GPU node và Kubernetes sẽ không thấy nvidia.com/gpu.
18. GPU node label/taint commands
kubectl label node gpu-node-1 accelerator=nvidia-l4
kubectl label node gpu-node-1 workload=ai-inference
kubectl taint node gpu-node-1 nvidia.com/gpu=true:NoSchedule
Undo khi lab xong:
kubectl label node gpu-node-1 accelerator-
kubectl label node gpu-node-1 workload-
kubectl taint node gpu-node-1 nvidia.com/gpu=true:NoSchedule-
19. Helm values skeleton cho RAG API
image:
repository: ghcr.io/acme/rag-api
tag: "2026.05.10-abcdef"
pullPolicy: IfNotPresent
replicaCount: 2
config:
APP_ENV: production
LOG_LEVEL: INFO
LLM_MODE: managed
LLM_PROVIDER: openai
LLM_MODEL: gpt-4.1-mini
QDRANT_URL: http://qdrant.rag-serving.svc.cluster.local:6333
QDRANT_COLLECTION: policy_chunks
secretRefs:
enabled: true
name: rag-api-secret
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "2"
memory: 4Gi
probes:
readiness:
path: /ready
initialDelaySeconds: 10
liveness:
path: /health
initialDelaySeconds: 30
nodeSelector: {}
tolerations: []
affinity: {}
20. KServe skeleton
KServe nên phục vụ model endpoint, không nhất thiết chứa toàn bộ RAG orchestration.
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: embedding-model
namespace: rag-serving
spec:
predictor:
model:
modelFormat:
name: sklearn
storageUri: s3://example-bucket/models/embedding-model
resources:
requests:
cpu: "1"
memory: "2Gi"
limits:
cpu: "2"
memory: "4Gi"
Template trên chỉ là skeleton để hiểu shape của InferenceService. Runtime/model format thật phụ thuộc stack bạn chọn: built-in runtime, custom runtime, Hugging Face runtime, vLLM runtime hoặc runtime nội bộ.
21. Ray Serve skeleton
from ray import serve
from starlette.requests import Request
@serve.deployment(num_replicas=2)
class Retriever:
async def __call__(self, query: str) -> list[dict]:
return await search_vector_db(query)
@serve.deployment(ray_actor_options={"num_gpus": 1})
class Reranker:
def __init__(self) -> None:
self.model = load_reranker()
async def __call__(self, query: str, docs: list[dict]) -> list[dict]:
return self.model.rerank(query, docs)
@serve.deployment
class RagApp:
def __init__(self, retriever, reranker) -> None:
self.retriever = retriever
self.reranker = reranker
async def __call__(self, request: Request) -> dict:
body = await request.json()
docs = await self.retriever.remote(body["question"])
ranked = await self.reranker.remote(body["question"], docs)
return {"contexts": ranked[:5]}
app = RagApp.bind(Retriever.bind(), Reranker.bind())
Ray Serve hữu ích khi bạn muốn scale Retriever, Reranker và model generation như các deployment riêng trong cùng inference graph.
22. Smoke test script
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${BASE_URL:-http://localhost:8000}"
curl -fsS "${BASE_URL}/health" | jq .
curl -fsS "${BASE_URL}/ready" | jq .
curl -fsS -X POST "${BASE_URL}/query" \
-H "Content-Type: application/json" \
-d '{
"question": "Nhân viên full-time có bao nhiêu ngày nghỉ phép năm?",
"tenant_id": "demo",
"user_roles": ["employee"]
}' | jq .
Nếu không muốn phụ thuộc jq, in raw response và kiểm tra status code.
23. Benchmark script skeleton
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${BASE_URL:-http://localhost:8000}"
N="${N:-20}"
for i in $(seq 1 "${N}"); do
start_ns=$(date +%s%N)
curl -fsS -o /tmp/rag-response.json -X POST "${BASE_URL}/query" \
-H "Content-Type: application/json" \
-d '{"question":"Chính sách remote work áp dụng thế nào?","tenant_id":"demo","user_roles":["employee"]}'
end_ns=$(date +%s%N)
ms=$(( (end_ns - start_ns) / 1000000 ))
echo "query=${i} latency_ms=${ms}"
done
Metric cần ghi lại:
- p50/p95/p99 latency.
- tokens input/output.
- LLM cost nếu dùng managed provider.
- CPU/RAM của API.
- Qdrant RAM/disk.
- GPU memory/utilization nếu dùng local model.
- Error rate, timeout rate, queue depth.
24. Deployment note template
# Deployment Note: RAG Serving
## Architecture
- API: FastAPI RAG orchestration.
- Vector DB: Qdrant.
- LLM: managed OpenAI by default, optional local model server.
- Storage: Qdrant PVC and document object storage.
## Runtime config
- ConfigMap: non-secret runtime config.
- Secret: provider API key.
- Index version: rag-v1.
- Prompt version: answer-v1.
## Local run
```bash
cp .env.example .env
docker compose up --build
scripts/smoke-test.sh
```
## Kubernetes run
```bash
kubectl apply -f k8s/namespace.yaml
kubectl apply -f k8s/configmap.yaml
kubectl apply -f k8s/secret.example.yaml
kubectl apply -f k8s/
```
## Resource estimate
| Component | CPU | RAM | GPU | Notes |
|---|---:|---:|---:|---|
| API | 500m-2 CPU | 1-4Gi | 0 | Depends on concurrency |
| Qdrant | 500m-2 CPU | 1-4Gi | 0 | Depends on corpus/vector dim |
| Model server | 2-8 CPU | 12-24Gi | 1 | Depends on model size/context |
## Rollback
- Roll back API image tag.
- Roll back prompt version.
- Roll back index version or restore vector DB snapshot.
- Disable local model server and switch to managed provider if GPU path fails.
## Production readiness
Answer: not production-ready until CI build/scanning, secret management, backup/restore, monitoring/alerts, load test and security review are complete.
25. Production checklist
Docker:
- Base image pinned.
- Dependencies locked.
-
.dockerignoreexcludes secret/data/cache/model files. - Image runs non-root.
- Health endpoint exists.
- CI builds and scans image.
- Production uses immutable tag or digest.
Compose:
-
docker compose up --buildworks from clean checkout. -
.env.exampledocuments all required variables. - Volumes are explicit.
- API healthcheck exists; vector DB readiness được kiểm tra qua API
/ready, Kubernetes probe hoặc healthcheck chính thức của image. - CPU path works without GPU.
- GPU path is optional profile.
Kubernetes:
- ConfigMap and Secret are separated.
- Requests/limits are set.
- Readiness/liveness probes are meaningful.
- RollingUpdate strategy is configured.
- Termination grace period handles streaming requests.
- Service is ClusterIP behind ingress/gateway.
- PDB/HPA are considered where appropriate.
GPU:
- Host driver installed.
- NVIDIA Container Toolkit configured.
-
nvidia-device-pluginrunning. -
kubectl describe nodeshowsnvidia.com/gpu. - GPU nodes labeled.
- GPU nodes tainted to avoid random workloads.
- GPU pods have
nodeSelector, tolerations andnvidia.com/gpulimit. - VRAM, GPU utilization and queue depth are monitored.
Security:
- No API key in image or Git.
- Authn/authz implemented.
- Tenant/ACL filter enforced server-side.
- Network policy considered.
- Upload size and file type restricted.
- Rate limiting and timeout configured.
Observability:
- Structured logs with request ID/trace ID.
- Latency metrics by stage: retrieve, rerank, generate.
- Token and cost metrics.
- Error/timeout metrics.
- Dashboards and alerts.
- Smoke test runs after deploy.
Data:
- Vector DB backup/restore tested.
- Metadata DB backup/restore tested.
- Document storage lifecycle defined.
- Index version and embedding model version recorded.
- Rollback plan for bad index release.
26. Nguồn tham khảo chính thức
- Docker build best practices: https://docs.docker.com/build/building/best-practices/
- Docker Compose services: https://docs.docker.com/reference/compose-file/services/
- Docker Compose startup order: https://docs.docker.com/compose/how-tos/startup-order/
- Qdrant monitoring endpoints: https://qdrant.tech/documentation/ops-monitoring/monitoring/
- Kubernetes GPU scheduling: https://kubernetes.io/docs/tasks/manage-gpus/scheduling-gpus/
- Kubernetes node selection: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/
- Kubernetes taints/tolerations: https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/
- NVIDIA Kubernetes device plugin: https://github.com/NVIDIA/k8s-device-plugin
- KServe overview: https://kserve.github.io/kserve/
- Ray Serve overview: https://docs.ray.io/en/latest/serve/
Bài tập
Mục tiêu
Bạn sẽ lấy RAG app từ Day 40 hoặc một FastAPI LLM service tương đương, rồi biến nó thành deployment package có thể review:
- Docker image cho API.
.dockerignore..env.example.- Docker Compose chạy API + vector DB.
- Health/readiness endpoint.
- Smoke test.
- Kubernetes manifests optional.
- GPU model server manifest optional.
- Deployment note trả lời production readiness.
Thời lượng đề xuất:
- Bản tối thiểu: 90 phút.
- Bản tốt cho portfolio: 0.5-1 ngày.
- Bản gần production hơn: 1-2 ngày, thêm CI, scanning, monitoring và backup.
0. Acceptance criteria
Hoàn thành bài tập khi bạn có:
-
docker compose up --buildchạy được CPU path. -
GET /healthtrả200. -
GET /readytrả200sau khi vector DB sẵn sàng. -
.env.exampleđủ để người khác tạo.env. - Docker image không chứa
.env, raw data lớn hoặc model cache lớn. - Compose có volume cho Qdrant/vector DB.
- Có smoke test query một câu hỏi mẫu.
- Có K8s manifests:
Deployment,Service,ConfigMap,Secret. - Có GPU manifest optional dùng
nodeSelector, tolerations vànvidia.com/gpu. - Có trade-off và production readiness answer trong README/deployment note.
1. Chuẩn bị app target
Nếu dùng Day 40, chọn backend FastAPI có endpoint query. Nếu chưa có app, tạo skeleton:
backend/
app/
main.py
health.py
settings.py
requirements.in
requirements.lock
API tối thiểu:
GET /health
GET /ready
POST /query
/query có thể gọi RAG pipeline thật hoặc mock response nếu bạn chỉ đang tập trung vào deployment. Nếu mock, README phải ghi rõ phần nào là mock.
2. Viết settings bằng environment variables
Tạo config contract:
APP_ENV
LOG_LEVEL
LLM_MODE
LLM_PROVIDER
LLM_MODEL
OPENAI_API_KEY
MODEL_SERVER_URL
EMBEDDING_MODEL
QDRANT_URL
QDRANT_COLLECTION
INDEX_VERSION
PROMPT_VERSION
REQUEST_TIMEOUT_SECONDS
MAX_CONCURRENT_REQUESTS
Yêu cầu:
- Không hard-code API key.
- Không hard-code URL
localhosttrong code backend; dùngQDRANT_URL. - Validate config khi app startup.
- Log config non-secret để debug.
3. Thêm health và readiness
/health kiểm tra process:
@router.get("/health")
async def health() -> dict[str, str]:
return {"status": "ok"}
/ready kiểm tra dependency nhẹ:
@router.get("/ready")
async def ready() -> JSONResponse:
checks = {
"qdrant": await check_qdrant(),
"index_version": await check_index_version(),
"llm_provider": await check_llm_provider(),
}
ok = all(checks.values())
return JSONResponse(
{"status": "ready" if ok else "not_ready", "checks": checks},
status_code=200 if ok else 503,
)
Không gọi full generation trong readiness probe. Probe chạy liên tục; gọi LLM thật có thể tốn tiền và làm service bị rate limit.
4. Tạo .dockerignore
.git
.venv
__pycache__
.pytest_cache
.mypy_cache
.ruff_cache
.env
.env.*
!.env.example
data/raw
data/uploads
data/vector_store
models
model_cache
reports
*.sqlite
*.db
*.parquet
*.pt
*.safetensors
Kiểm tra lại:
docker build --no-cache -t rag-api:test ./backend
Nếu build context quá lớn, .dockerignore chưa đủ tốt.
5. Viết Dockerfile API
Tạo backend/Dockerfile:
# syntax=docker/dockerfile:1.7
FROM python:3.11-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
APP_HOME=/app
WORKDIR ${APP_HOME}
RUN groupadd --system app && useradd --system --gid app --home-dir ${APP_HOME} app
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.lock ./requirements.lock
RUN \
pip install --upgrade pip \
&& pip install -r requirements.lock
COPY app ./app
RUN chown -R app:app ${APP_HOME}
USER app
EXPOSE 8000
HEALTHCHECK \
CMD curl -fsS http://127.0.0.1:8000/health || exit 1
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"]
Build:
docker build -t rag-api:local ./backend
Run thử:
docker run --rm --env-file .env -p 8000:8000 rag-api:local
curl -fsS http://localhost:8000/health
6. Tạo .env.example
APP_ENV=local
APP_NAME=rag-serving
LOG_LEVEL=INFO
PORT=8000
WORKERS=1
REQUEST_TIMEOUT_SECONDS=60
MAX_UPLOAD_MB=25
MAX_CONCURRENT_REQUESTS=16
LLM_MODE=managed
LLM_PROVIDER=openai
LLM_MODEL=gpt-4.1-mini
OPENAI_API_KEY=replace-me
MODEL_SERVER_URL=http://model-server:8001/v1
MODEL_ID=meta-llama/Llama-3.1-8B-Instruct
MODEL_CACHE_DIR=/models/huggingface
EMBEDDING_PROVIDER=openai
EMBEDDING_MODEL=text-embedding-3-small
EMBEDDING_DIM=1536
VECTOR_DB=qdrant
QDRANT_URL=http://qdrant:6333
QDRANT_COLLECTION=policy_chunks
INDEX_VERSION=rag-v1
PROMPT_VERSION=answer-v1
CHUNKING_VERSION=chunk-v1
OTEL_EXPORTER_OTLP_ENDPOINT=
TRACE_SAMPLE_RATE=1.0
Tạo .env local:
cp .env.example .env
Không commit .env.
7. Viết Docker Compose
Tạo docker-compose.yml:
name: rag-serving
services:
api:
build:
context: ./backend
dockerfile: Dockerfile
image: rag-serving-api:${APP_IMAGE_TAG:-local}
env_file:
- .env
environment:
QDRANT_URL: http://qdrant:6333
ports:
- "8000:8000"
depends_on:
qdrant:
condition: service_started
volumes:
- ./data/sample_docs:/app/data/sample_docs:ro
- ./reports:/app/reports
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
restart: unless-stopped
qdrant:
image: qdrant/qdrant:${QDRANT_TAG:-v1.14.1}
ports:
- "6333:6333"
volumes:
- qdrant_data:/qdrant/storage
restart: unless-stopped
volumes:
qdrant_data:
Lưu ý: Qdrant có endpoint /readyz, nhưng Compose healthcheck chạy bên trong container. Nếu image không có curl/wget, đừng thêm healthcheck giòn; thay vào đó /ready của API phải retry Qdrant và trả 503 cho đến khi dependency sẵn sàng.
Chạy:
docker compose up --build
Kiểm tra:
curl -fsS http://localhost:8000/health
curl -fsS http://localhost:8000/ready
8. Thêm optional GPU profile
Nếu bạn có local model server, thêm service:
model-server:
profiles: ["gpu"]
build:
context: ./model-server
dockerfile: Dockerfile.gpu
image: rag-model-server:${MODEL_IMAGE_TAG:-local}
env_file:
- .env
ports:
- "8001:8001"
volumes:
- model_cache:/models/huggingface
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: ["gpu"]
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8001/health"]
interval: 30s
timeout: 5s
retries: 10
start_period: 120s
restart: unless-stopped
Thêm volume:
volumes:
qdrant_data:
model_cache:
Chạy:
docker compose --profile gpu up --build
Trước khi chạy GPU path, kiểm tra:
docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi
9. Viết smoke test
Tạo scripts/smoke-test.sh:
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${BASE_URL:-http://localhost:8000}"
curl -fsS "${BASE_URL}/health"
echo
curl -fsS "${BASE_URL}/ready"
echo
curl -fsS -X POST "${BASE_URL}/query" \
-H "Content-Type: application/json" \
-d '{
"question": "Nhân viên full-time có bao nhiêu ngày nghỉ phép năm?",
"tenant_id": "demo",
"user_roles": ["employee"]
}'
echo
Chạy:
chmod +x scripts/smoke-test.sh
scripts/smoke-test.sh
10. Viết Kubernetes manifests
Tạo các file trong k8s/.
configmap.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: rag-api-config
data:
APP_ENV: "production"
LOG_LEVEL: "INFO"
LLM_MODE: "managed"
LLM_PROVIDER: "openai"
LLM_MODEL: "gpt-4.1-mini"
EMBEDDING_MODEL: "text-embedding-3-small"
QDRANT_URL: "http://qdrant:6333"
QDRANT_COLLECTION: "policy_chunks"
INDEX_VERSION: "rag-v1"
PROMPT_VERSION: "answer-v1"
secret.example.yaml:
apiVersion: v1
kind: Secret
metadata:
name: rag-api-secret
type: Opaque
stringData:
OPENAI_API_KEY: "replace-in-secret-manager"
api-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: rag-api
labels:
app: rag-api
spec:
replicas: 2
selector:
matchLabels:
app: rag-api
template:
metadata:
labels:
app: rag-api
spec:
terminationGracePeriodSeconds: 60
containers:
- name: api
image: ghcr.io/your-org/rag-api:replace-with-tag
ports:
- name: http
containerPort: 8000
envFrom:
- configMapRef:
name: rag-api-config
- secretRef:
name: rag-api-secret
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 10
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 30
api-service.yaml:
apiVersion: v1
kind: Service
metadata:
name: rag-api
spec:
type: ClusterIP
selector:
app: rag-api
ports:
- name: http
port: 8000
targetPort: http
Validate dry-run nếu có cluster context:
kubectl apply --dry-run=client -f k8s/
11. Optional GPU Kubernetes manifest
Chuẩn bị node:
kubectl label node gpu-node-1 accelerator=nvidia-l4
kubectl label node gpu-node-1 workload=ai-inference
kubectl taint node gpu-node-1 nvidia.com/gpu=true:NoSchedule
model-server-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: model-server
labels:
app: model-server
spec:
replicas: 1
selector:
matchLabels:
app: model-server
template:
metadata:
labels:
app: model-server
spec:
terminationGracePeriodSeconds: 120
nodeSelector:
accelerator: nvidia-l4
workload: ai-inference
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
containers:
- name: model-server
image: ghcr.io/your-org/rag-model-server:replace-with-tag
ports:
- name: http
containerPort: 8001
env:
- name: MODEL_ID
value: "meta-llama/Llama-3.1-8B-Instruct"
resources:
requests:
cpu: "2"
memory: "12Gi"
limits:
cpu: "8"
memory: "24Gi"
nvidia.com/gpu: 1
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 120
periodSeconds: 15
failureThreshold: 20
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 180
periodSeconds: 30
Kiểm tra GPU resource:
kubectl describe node gpu-node-1 | grep -A5 "nvidia.com/gpu"
kubectl describe pod <model-server-pod>
12. Benchmark
Chạy ít nhất 10 query:
for i in $(seq 1 10); do
time curl -fsS -X POST http://localhost:8000/query \
-H "Content-Type: application/json" \
-d '{"question":"Chính sách remote work áp dụng thế nào?","tenant_id":"demo","user_roles":["employee"]}' \
>/tmp/rag-response.json
done
Ghi vào reports/deployment-benchmark.md:
| Metric | Kết quả |
|---|---|
| p50 latency | |
| p95 latency | |
| API CPU/RAM | |
| Qdrant RAM/disk | |
| GPU memory | optional |
| Error rate | |
| Token cost/query | nếu dùng managed LLM |
13. Trade-off phải ghi trong README
Trả lời các câu sau:
- Vì sao dùng managed LLM hay local model?
- Vì sao Compose đủ cho local nhưng chưa đủ cho production?
- Nếu deploy Kubernetes, API và model server scale khác nhau thế nào?
- Image có nên chứa model không, hay mount model cache?
- Vector DB tự vận hành hay dùng managed service?
- GPU node có cần taint không, vì sao?
- HPA theo CPU có đủ cho GPU inference không?
Gợi ý câu trả lời ngắn:
Với capstone, managed LLM là lựa chọn mặc định vì giảm GPU ops và giúp reviewer chạy được nhanh.
Local GPU model chỉ bật khi có yêu cầu privacy/cost/latency rõ và đã benchmark VRAM.
Compose dùng cho local review; production cần Kubernetes hoặc platform tương đương để có rollout, secret, scaling, network policy, observability và backup.
14. Production readiness answer
Trong README hoặc deployment note, trả lời trực tiếp:
## Dùng được trong production không?
Chưa, nếu chỉ dùng cấu hình lab. Có thể đưa vào production sau khi hoàn thành các điều kiện sau:
- CI build image, lock dependencies, scan vulnerability và tag immutable.
- Secret nằm trong secret manager, không nằm trong Git/image.
- Authn/authz và tenant/ACL filter chạy server-side.
- `/ready` kiểm tra vector DB, model provider và index version.
- Resource requests/limits được sizing bằng benchmark.
- Vector DB/document store có backup/restore test.
- Logs/metrics/traces có dashboard và alert.
- Có rate limit, timeout, upload limit và graceful shutdown.
- Có rollout/smoke test/rollback plan cho image, prompt, model và index.
- Nếu dùng GPU: node driver/toolkit/device plugin ổn định, có monitoring VRAM/GPU utilization và plan xử lý GPU OOM.
Nếu bạn muốn ghi "có thể dùng production", hãy ghi kèm phạm vi:
Có thể dùng production cho internal workload traffic thấp/trung bình nếu triển khai trên Kubernetes hoặc platform tương đương, dùng managed LLM/vector DB, bật auth/ACL, có backup, monitoring, rate limit và đã pass load test theo SLO.
Chưa phù hợp cho dữ liệu nhạy cảm hoặc traffic cao nếu chưa có security review, data governance và capacity planning.
15. Submission checklist
Nộp các artifact:
-
backend/Dockerfile. -
.dockerignore. -
.env.example. -
docker-compose.yml. -
scripts/smoke-test.sh. -
k8s/configmap.yaml. -
k8s/secret.example.yaml. -
k8s/api-deployment.yaml. -
k8s/api-service.yaml. - Optional
k8s/model-server-deployment.yaml. -
reports/deployment-benchmark.md. - README/deployment note có trade-off và production readiness answer.
16. Rubric
| Hạng mục | Điểm | Tiêu chí |
|---|---|---|
| Docker image | 20 | Build được, dependency locked, non-root, không leak secret/data |
| Compose | 20 | API + vector DB chạy được, healthcheck, volume, .env.example |
| Health/readiness | 15 | /health nhẹ, /ready kiểm tra dependency đúng |
| Kubernetes | 15 | Deployment/Service/ConfigMap/Secret, resource, probes |
| GPU awareness | 10 | Hiểu NVIDIA stack, có manifest nodeSelector/toleration/GPU limit |
| Smoke test/benchmark | 10 | Có script và báo cáo latency/resource |
| Trade-off/production readiness | 10 | Trả lời rõ dùng được production không và cần điều kiện gì |
17. Lỗi thường gặp
- Commit
.envhoặc API key. - Docker image copy cả
data/,.git,.venv, model cache. - Compose dùng
localhostbên trong container để gọi service khác. /readyluôn trả200dù vector DB chết.- Dùng
latestcho production image. - Kubernetes manifest không có resource requests/limits.
- GPU pod thiếu
nvidia.com/gpu. - GPU pod có toleration nhưng không có
nodeSelector, dẫn đến scheduling không như kỳ vọng. - GPU node không taint, workload thường chạy vào GPU node.
- HPA chỉ nhìn CPU trong khi bottleneck thật là tokens/sec, queue depth hoặc VRAM.
18. Câu hỏi tự kiểm tra
- Docker image của bạn có chạy được nếu không mount source code không?
- Người khác có thể tạo
.envchỉ từ.env.examplekhông? - Nếu Qdrant chưa ready, API có nhận traffic không?
- Nếu LLM provider timeout, request có timeout và log trace ID không?
- Nếu rolling deploy xảy ra trong lúc streaming, graceful shutdown xử lý thế nào?
- Nếu index version mới lỗi, rollback bằng cách nào?
- Nếu GPU OOM, bạn nhìn metric/log nào trước?
- Nếu traffic tăng 5 lần, bạn scale API, vector DB hay model server trước?