Cơn ác mộng 2 giờ sáng: “Nó chạy trên máy em mà?”
Tôi nhớ như in một đêm trực server, hệ thống báo lỗi 500 liên tục ngay sau khi deploy. Log từ container FastAPI chỉ vỏn vẹn đúng một dòng: sqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation "users" does not exist.
Mọi thứ ở local vẫn chạy mượt mà. Vấn đề là tôi đã quên chạy migration của Alembic khi deploy. Tệ hơn nữa, cái Dockerfile cũ kỹ nặng tới 1.2GB khiến việc pull image mới để fix lỗi mất cả thanh xuân.
Nếu bạn đang gặp tình trạng container ì ạch, image nặng hàng GB, hoặc lúng túng không biết chạy Alembic thế nào trong Docker, bài viết này dành cho bạn. Đây là những kinh nghiệm xương máu mình đúc kết sau hàng chục lần “ăn hành” trên production.
Tại sao Dockerfile “mì ăn liền” lại gây họa?
Đa số chúng ta thường bắt đầu với một Dockerfile đơn giản như thế này:
FROM python:3.11
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Nhìn thì có vẻ ổn, nhưng thực tế nó là một “quả bom nổ chậm” với 3 vấn đề lớn:
- Kích thước khổng lồ: Image chứa cả compiler, cache của pip và các file rác.
- Bảo mật kém: Chạy container bằng quyền root cực kỳ nguy hiểm nếu hacker xâm nhập được vào app.
- DB bị lệch pha: Database không tự cập nhật schema khi bạn cập nhật code.
Chiến lược Multi-stage Build: Ép cân từ 1GB xuống 150MB
Để giải quyết vấn đề kích thước, Multi-stage build là lựa chọn hàng đầu. Ý tưởng rất đơn giản: Dùng một image đầy đủ công cụ để build dependencies, sau đó chỉ bốc những thứ cần thiết sang một image “siêu nhẹ” (slim) để chạy.
Một mẹo nhỏ khi debug Docker API: Nếu cần đọc JSON response nhanh, bạn có thể paste vào toolcraft.app/vi/tools/developer/json-formatter. Nó giúp định dạng lại dữ liệu cực nhanh mà không cần cài thêm extension rườm rà.
Dưới đây là cấu hình Dockerfile tối ưu mà tôi thường dùng cho các dự án thực tế:
# Stage 1: Builder - Nơi cài đặt compiler và build wheels
FROM python:3.11-slim as builder
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
RUN apt-get update && apt-get install -y --no-install-recommends gcc python3-dev libpq-dev
COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt
# Stage 2: Final - Image chạy Production cực gọn
FROM python:3.11-slim
WORKDIR /app
# Tạo user riêng để bảo mật, không dùng root
RUN addgroup --system app && adduser --system --group app
# Chỉ cài thư viện runtime cần thiết
RUN apt-get update && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/wheels /wheels
COPY --from=builder /app/requirements.txt .
RUN pip install --no-cache /wheels/*
COPY . .
# Cấp quyền cho user app
RUN chown -R app:app /app
USER app
CMD ["/app/entrypoint.sh"]
Xử lý migrations với Alembic: Tự động hóa hoàn toàn
Đừng bao giờ đợi deploy xong mới gõ alembic upgrade head bằng tay. Hãy đẩy nó vào file entrypoint.sh. File này đảm bảo database luôn sẵn sàng và đúng version trước khi FastAPI khởi động.
#!/bin/sh
echo "Đang kiểm tra kết nối Database..."
# Bạn nên dùng script wait-for-it.sh để chắc chắn DB đã sẵn sàng
echo "Đang chạy migrations..."
alembic upgrade head
echo "Khởi động server..."
exec "$@"
Nhớ cấp quyền thực thi: chmod +x entrypoint.sh. Nếu thiếu bước này, container của bạn sẽ báo lỗi “Permission denied” ngay lập tức.
Docker Compose: Tách biệt Dev và Production
Khi code ở local, chúng ta cần --reload để container tự nhận thay đổi mỗi khi nhấn Ctrl+S. Tuy nhiên, trên Production, --reload là tối kỵ vì nó làm giảm hiệu năng và gây mất ổn định.
Giải pháp là dùng docker-compose.yml linh hoạt:
services:
db:
image: postgres:15-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=myuser
- POSTGRES_PASSWORD=mypass
- POSTGRES_DB=fastapi_db
web:
build: .
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
volumes:
- .:/app
ports:
- "8000:8000"
env_file:
- .env
depends_on:
- db
volumes:
postgres_data:
Lưu ý: Trong file .env, hãy đổi DATABASE_URL từ localhost thành db. Đây là tên service trong file compose giúp các container tìm thấy nhau trong mạng nội bộ.
Nâng cấp lên Gunicorn cho Production
Khi go-live, hãy thay uvicorn bằng Gunicorn để tận dụng cơ chế đa nhân (multi-worker). Gunicorn quản lý các process cực tốt. Nếu một worker bị treo, nó sẽ tự khởi động lại worker khác để giữ app luôn sống.
gunicorn -w 4 -k uvicorn.workers.UvicornWorker app.main:app --bind 0.0.0.0:8000
Công thức tính số worker thông thường là: (2 x số core CPU) + 1. Ví dụ server có 2 core, bạn nên set 5 workers.
Quy trình chuẩn khi sửa Model SQLAlchemy
Để tránh lỗi lệch schema, hãy tuân thủ 5 bước này:
- Cập nhật model trong code Python.
- Chạy
docker-compose exec web alembic revision --autogenerate -m "mô tả thay đổi". - Kiểm tra lại file trong folder
alembic/versionsđể đảm bảo không có gì sai sót. - Commit cả code và file migration mới lên Git.
- Khi deploy,
entrypoint.shsẽ lo phần việc còn lại trên server.
Việc Dockerize FastAPI không chỉ là copy-paste vài dòng lệnh. Đó là cách bạn tối ưu layer, bảo mật quyền user và đồng bộ dữ liệu. Làm đúng ngay từ đầu giúp bạn kê cao gối ngủ ngon mà không lo những cuộc gọi lúc nửa đêm.

