Tập 15 đã có backup tự động và diễn tập restore, nhưng backup chỉ có ích khi ứng dụng chạy lại được. Tập này đóng gói app quản lý chi tiêu thành image Docker đúng chuẩn production: build nhiều tầng để image gọn nhẹ, chạy bằng user không phải root, và gắn healthcheck để hệ thống biết app còn sống hay đã treo.
1. Multi-stage build: image nhỏ, ít bề mặt tấn công
Một Dockerfile một tầng thường kéo cả compiler và cache pip vào image cuối — image phình lên cả gigabyte và mang theo công cụ mà kẻ tấn công có thể dùng. Multi-stage build tách việc build ra tầng riêng, tầng chạy chỉ giữ lại những gì cần thiết.
# Dockerfile# Tầng build: cài dependencies vào venv riêngFROM python:3.14-slim AS builderWORKDIR /appCOPY requirements.txt .RUN python -m venv /opt/venv \ && /opt/venv/bin/pip install --no-cache-dir -r requirements.txt# Tầng chạy: chỉ giữ venv + code, không còn pip cache hay compilerFROM python:3.14-slimWORKDIR /appCOPY --from=builder /opt/venv /opt/venvCOPY . .ENV PATH="/opt/venv/bin:$PATH"ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1EXPOSE 8000CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
Image kết quả thường chỉ còn khoảng 150–200 MB thay vì hơn 1 GB. Dùng tag cố định như python:3.14-slim thay vì latest để lần build sau không âm thầm đổi phiên bản Python.
2. Chạy bằng user không phải root
Mặc định, process trong container chạy bằng root. Nếu kẻ tấn công thoát được khỏi container, hắn có quyền root trên host. Tạo một user riêng và chuyển sang user đó trước khi chạy app.
# Thêm vào tầng chạy, trước dòng CMDRUN groupadd -r appuser && useradd -r -g appuser appuser \ && mkdir -p /app/staticfiles /app/media \ && chown -R appuser:appuser /appUSER appuser
Mọi lệnh cần quyền root phải chạy trước dòng USER. Thư mục static và media phải thuộc về appuser, nếu không collectstatic và upload file sẽ lỗi Permission denied ngay lần deploy đầu tiên.
3. Healthcheck: để hệ thống tự phát hiện app treo
App vẫn chạy nhưng không trả lời request — ví dụ pool database kẹt — thì restart thủ công là quá chậm. Healthcheck gọi định kỳ vào endpoint /health đã làm ở tập 8; nếu endpoint lỗi liên tục, Docker đánh dấu container unhealthy để orchestrator restart.
# Thêm vào tầng chạy, trước dòng CMDHEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health/')"
Dùng urllib có sẵn trong Python thay vì cài thêm curl, image khỏi phình thêm. Tham số start-period=40s cho app thời gian khởi động và chạy migrations, tránh bị kết luận chết oan ngay khi vừa lên.
4. Ghép lại bằng docker-compose cho production
Một file compose mô tả đủ web, database và Redis, kèm depends_on chờ database khỏe hẳn rồi web mới chạy.
# docker-compose.prod.ymlservices: web: build: . env_file: .env.prod ports: - "8000:8000" depends_on: db: condition: service_healthy restart: unless-stopped db: image: postgres:17 env_file: .env.prod volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"] interval: 10s timeout: 5s retries: 5 redis: image: redis:8-alpine restart: unless-stoppedvolumes: pgdata:
Chạy bằng docker compose -f docker-compose.prod.yml up -d --build, kiểm tra bằng docker compose ps. Nhớ chạy migrations trong container web trước khi mở traffic: docker compose exec web python manage.py migrate.
Áp dụng vào dự án của bạn
Áp dụng ngay cho app quản lý chi tiêu: viết lại Dockerfile theo mẫu multi-stage ở trên, chạy bằng user appuser, gắn healthcheck vào endpoint /health đã có từ tập 8, rồi deploy bằng file compose production. Sau khi deploy, kiểm tra: image dưới 250 MB, docker compose ps báo web healthy, /health trả về 200. Từ tập sau, pipeline CI/CD sẽ build image này tự động mỗi lần push code.
Bài viết liên quan
- Django vận hành production #15: Backup và restore — pg_dump tự động, backup media, diễn tập restore định kỳ
- Django vận hành production #14: Upload file và media an toàn — validate loại file, giới hạn dung lượng, signed URL
- Django vận hành production #3: Gunicorn + Nginx — chạy Django sau reverse proxy
Câu hỏi thường gặp
Image production nên nhỏ cỡ nào?
Khoảng 150–250 MB với Python slim là hợp lý. Image trên 1 GB thường do mang theo build tools, cache pip hoặc copy cả thư mục .git — thêm .dockerignore loại bỏ .git, __pycache__, .env và node_modules nếu có.
Chạy non-root có làm mất tính năng gì không?
Không, nếu nhớ chown đúng thư mục app cần ghi. Lỗi thường gặp nhất là collectstatic và upload media bị Permission denied — tạo thư mục và chown trước dòng USER là xong.
Healthcheck khác gì với endpoint /health ở tập 8?
Endpoint /health là nơi app tự báo cáo trạng thái, còn healthcheck là cơ chế Docker gọi endpoint đó định kỳ. Không có healthcheck, Docker chỉ biết container còn chạy, không biết app bên trong đã treo hay chưa.
Nguồn tham khảo
- Docker — Multi-stage builds: https://docs.docker.com/build/building/multi-stage/
- Docker — HEALTHCHECK instruction: https://docs.docker.com/reference/dockerfile/#healthcheck
- Django — Deployment checklist: https://docs.djangoproject.com/en/stable/howto/deployment/checklist/
