Tóm tắt: Chạy fastapi run (là uvicorn) với --workers N trong container non-root → Nginx terminate TLS và proxy_pass tới container → bật tin cậy header X-Forwarded-* bằng --forwarded-allow-ips để app biết scheme/IP thật → nếu public dưới /netinv/ thì đặt root_path. Đã chạy thử với FastAPI 0.162, uvicorn 0.54, gunicorn 26 và Docker 27.
Luồng hoạt động
flowchart LR C[Client] -->|"HTTPS /netinv/..."| N["Nginx (TLS)"] N -->|"HTTP, X-Forwarded-For/Proto"| U["uvicorn master"] U --> W1[worker 1] U --> W2[worker 2] W1 --> A["FastAPI app (root_path=/netinv)"] W2 --> A
Cấu hình
Chọn cách chạy
| Cách | Lệnh | Dùng khi |
|---|---|---|
fastapi run |
fastapi run app/main.py --port 8000 --workers 4 |
Mặc định, đủ cho đa số trường hợp |
| uvicorn trực tiếp | uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4 |
Cần flag uvicorn chưa có trong fastapi run |
| gunicorn + uvicorn worker | gunicorn app.main:app -k uvicorn_worker.UvicornWorker -w 4 -b 0.0.0.0:8000 |
Quen quản lý bằng gunicorn (--timeout, --max-requests, reload bằng HUP) |
| 1 worker/container | fastapi run app/main.py --port 8000 |
Kubernetes: scale bằng số replica, không bằng worker |
- Số worker khởi điểm: bằng số CPU core cấp cho container, sau đó đo tải. Mỗi worker là một process riêng: RAM nhân lên, biến global/cache không chia sẻ giữa các worker.
- Gunicorn: class
uvicorn.workers.UvicornWorkercũ đã deprecated, dùng góiuvicorn-worker(uv add gunicorn uvicorn-worker).
Dockerfile với uv
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:0.11 /uv /uvx /bin/
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_PYTHON_DOWNLOADS=0
WORKDIR /app
# Cài dependency trước (layer cache), chỉ đổi khi uv.lock đổi
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --locked --no-install-project --no-dev
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev
FROM python:3.12-slim
RUN useradd --system --uid 10001 app
COPY --from=builder --chown=app:app /app /app
ENV PATH="/app/.venv/bin:$PATH"
WORKDIR /app
USER app
EXPOSE 8000
CMD ["fastapi", "run", "app/main.py", "--port", "8000", "--workers", "2", "--forwarded-allow-ips", "*"].venv
__pycache__
.git
.envdocker build -t netinv:0.1.0 .
docker run -d --name netinv -p 127.0.0.1:8000:8000 --env-file .env netinv:0.1.0--forwarded-allow-ips "*" nghĩa là tin header X-Forwarded-* từ bất kỳ ai gửi tới. Chỉ dùng khi container không publish port ra ngoài, chỉ Nginx gọi được (publish vào 127.0.0.1 như trên hoặc chung Docker network nội bộ). Nếu không thì liệt kê IP/subnet của proxy. Thêm về Dockerfile xem Dockerfile best practices.
Nginx phía trước
location /netinv/ {
proxy_pass http://127.0.0.1:8000/; # dấu / cuối: bỏ prefix /netinv trước khi gửi
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
}Server block TLS đầy đủ xem Nginx reverse proxy TLS.
Proxy headers và root_path
| Thiết lập | Tác dụng |
|---|---|
--proxy-headers (uvicorn bật sẵn) |
Đọc X-Forwarded-Proto, X-Forwarded-For để request.url.scheme là https, request.client.host là IP client thật |
--forwarded-allow-ips |
IP được tin. Mặc định 127.0.0.1 (hoặc env FORWARDED_ALLOW_IPS): đủ khi Nginx cùng host, không đủ khi Nginx ở container/máy khác |
FastAPI(root_path="/netinv") hoặc --root-path /netinv |
App biết mình nằm dưới prefix: /docs gọi đúng /netinv/openapi.json, servers trong OpenAPI là /netinv |
from fastapi import FastAPI, Request
app = FastAPI(root_path="/netinv")
@app.get("/info")
def info(request: Request) -> dict[str, str | None]:
# sau Nginx: scheme=https, client=IP thật của người dùng
return {"scheme": request.url.scheme, "client": request.client.host if request.client else None}Kiểm tra nhanh mà không cần Nginx: curl -s localhost:8000/info -H "X-Forwarded-Proto: https" -H "X-Forwarded-For: 203.0.113.9".
Chạy bằng systemd (không Docker)
[Unit]
Description=NetInventory API
After=network-online.target
[Service]
User=netinv
WorkingDirectory=/opt/netinv
EnvironmentFile=/etc/netinv.env
ExecStart=/opt/netinv/.venv/bin/fastapi run app/main.py --host 127.0.0.1 --port 8000 --workers 4
Restart=on-failure
[Install]
WantedBy=multi-user.targetTạo venv bằng uv sync --locked --no-dev trong /opt/netinv. Thêm tuỳ chọn hardening xem systemd service.
Lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách xử lý |
|---|---|---|
/docs trắng, console báo 404 /openapi.json |
App chạy dưới prefix nhưng không đặt root_path |
FastAPI(root_path="/netinv") hoặc --root-path /netinv |
Redirect (vd thêm / cuối) về http:// thay vì https:// |
App không tin header từ proxy | Nginx gửi X-Forwarded-Proto, uvicorn --forwarded-allow-ips gồm IP của Nginx |
| Log chỉ thấy IP của Nginx/Docker gateway | Như trên, X-Forwarded-For bị bỏ qua |
Như trên |
| 404 mọi đường dẫn sau Nginx | proxy_pass không có / cuối nên prefix bị gửi nguyên, app lại không đặt root_path |
Thêm / cuối vào proxy_pass, hoặc đặt root_path cho khớp prefix |
502 Bad Gateway |
App listen 127.0.0.1 trong container |
Trong container listen 0.0.0.0 (fastapi run mặc định đã vậy) |
504 Gateway Timeout ở request dài |
proxy_read_timeout mặc định 60s |
Tăng timeout cho location đó, hoặc chuyển việc dài sang background job |
| Dữ liệu cache/session lúc có lúc không | Nhiều worker, mỗi worker giữ state riêng trong RAM | Đưa state ra Redis/DB |
| Container chạy root | Thiếu USER |
Tạo user và USER app như Dockerfile trên |
DeprecationWarning khi dùng uvicorn.workers.UvicornWorker |
Class cũ đã chuyển sang gói riêng | -k uvicorn_worker.UvicornWorker |