文章目录
-
- 前言
- 一、最终部署架构
- [二、创建 FastAPI 项目](#二、创建 FastAPI 项目)
- 三、配置项目依赖
- [四、编写 Dockerfile](#四、编写 Dockerfile)
-
- [1. 基础镜像](#1. 基础镜像)
- [2. 工作目录](#2. 工作目录)
- [3. 安装 Python 依赖](#3. 安装 Python 依赖)
- [4. 声明容器端口](#4. 声明容器端口)
- [5. 启动 FastAPI](#5. 启动 FastAPI)
- [五、配置 Docker Compose](#五、配置 Docker Compose)
- [六、为什么使用 127.0.0.1](#六、为什么使用 127.0.0.1)
- 七、配置环境变量
- [八、配置 Docker 忽略文件](#八、配置 Docker 忽略文件)
- [九、构建 Docker 镜像](#九、构建 Docker 镜像)
- 十、启动容器
- 十一、验证接口
-
- [1. 查看端口映射](#1. 查看端口映射)
- [2. 测试健康检查](#2. 测试健康检查)
- [3. 访问 Swagger](#3. 访问 Swagger)
- [4. 测试业务接口](#4. 测试业务接口)
- 十二、修改宿主机映射端口
- [十三、配置 Nginx 反向代理](#十三、配置 Nginx 反向代理)
- [十四、配置 HTTPS](#十四、配置 HTTPS)
- [十五、不使用 Docker Compose 的写法](#十五、不使用 Docker Compose 的写法)
- 踩坑记录
-
- [问题一:Docker Hub连接超时](#问题一:Docker Hub连接超时)
- [问题二:`pip install` 十几分钟没有完成](#问题二:
pip install十几分钟没有完成) - [问题三:Dockerfile 出现 unknown instruction](#问题三:Dockerfile 出现 unknown instruction)
- 问题四:宿主机端口被占用
- 问题五:修改端口后没有生效
- 问题六:健康检查失败
- 十六、常用命令汇总
- 总结
前言
最近需要把一个 Python 接口部署到服务器,并提供给业务方调用。
项目使用 FastAPI 开发,部署时主要有下面几个需求:
- 使用 Docker 打包并运行 FastAPI。
- 将容器里的
8000端口映射到宿主机的18000端口。 - FastAPI 端口不直接暴露到公网。
- 使用 Nginx 对外提供访问。
- 后续可以继续配置域名和 HTTPS。
部署过程看起来不复杂,但实际操作时可能遇到以下问题:
- Docker Hub连接超时;
python:3.12-slim镜像拉取失败;pip install十几分钟没有反应;- Dockerfile 提示
unknown instruction; - 不清楚宿主机端口和容器端口的区别;
- 修改端口后没有生效。
本文记录一套完整的部署过程,并把这些常见问题集中整理出来。
一、最终部署架构
部署完成后的访问关系如下:
text
业务方
↓
HTTPS 443
↓
Nginx
↓
http://127.0.0.1:18000
↓
Docker端口映射
↓
FastAPI容器 8000
本次使用的端口:
| 服务位置 | 端口 |
|---|---|
| FastAPI 容器内部 | 8000 |
| Docker 宿主机 | 18000 |
| Nginx 对外端口 | 80/443 |
端口映射关系:
text
宿主机 127.0.0.1:18000 → Docker 容器 8000
二、创建 FastAPI 项目
项目目录如下:
text
order-api/
├── app/
│ ├── __init__.py
│ └── main.py
├── .dockerignore
├── .env
├── Dockerfile
├── compose.yaml
└── requirements.txt
创建 app/main.py:
python
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI(
title="订单服务",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc",
)
class OrderRequest(BaseModel):
customer_id: str
amount: float
@app.get("/health")
def health():
return {
"status": "ok"
}
@app.get("/ready")
def ready():
return {
"status": "ready"
}
@app.post("/api/v1/orders")
def create_order(request: OrderRequest):
return {
"code": "0",
"message": "success",
"data": {
"customer_id": request.customer_id,
"amount": request.amount,
},
}
这里一共提供了三个接口:
text
GET /health
GET /ready
POST /api/v1/orders
其中 /health 用于检查服务是否正常运行。
FastAPI 还会自动生成 Swagger 文档:
text
http://服务地址/docs
三、配置项目依赖
创建 requirements.txt:
text
fastapi[standard]
如果项目中使用了数据库、Redis 等组件,可以继续添加:
text
sqlalchemy
pymysql
redis
alembic
生产环境建议固定依赖版本,避免某个依赖发布新版本后导致镜像重新构建失败。
例如:
text
fastapi[standard]==经过测试的版本
四、编写 Dockerfile
创建 Dockerfile:
dockerfile
FROM public.ecr.aws/docker/library/python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /code
COPY requirements.txt /code/requirements.txt
ARG PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
RUN python -m pip install \
--no-cache-dir \
--disable-pip-version-check \
--timeout 60 \
--retries 3 \
--index-url "${PIP_INDEX_URL}" \
-r /code/requirements.txt
COPY app /code/app
RUN useradd --create-home --uid 10001 appuser
USER appuser
EXPOSE 8000
CMD ["fastapi", "run", "app/main.py", "--port", "8000", "--proxy-headers", "--forwarded-allow-ips=*"]
下面简单说明几个关键配置。
1. 基础镜像
dockerfile
FROM public.ecr.aws/docker/library/python:3.12-slim
正常情况下可以直接使用:
dockerfile
FROM python:3.12-slim
但是如果当前网络访问 Docker Hub 超时,可以尝试 AWS 公共镜像仓库中的镜像:
dockerfile
FROM public.ecr.aws/docker/library/python:3.12-slim
2. 工作目录
dockerfile
WORKDIR /code
后续复制的代码和执行的命令都以 /code 为工作目录。
3. 安装 Python 依赖
dockerfile
RUN python -m pip install \
--no-cache-dir \
--disable-pip-version-check \
--timeout 60 \
--retries 3 \
--index-url "${PIP_INDEX_URL}" \
-r /code/requirements.txt
如果默认 PyPI 下载速度较慢,可以通过 PIP_INDEX_URL 指定一个可访问的镜像。
公司内部如果有 Python 制品仓库,优先使用公司仓库。
4. 声明容器端口
dockerfile
EXPOSE 8000
需要注意:
EXPOSE 8000只说明程序在容器中使用8000端口,并不会自动把该端口映射到宿主机。
真正的端口映射需要在 Docker Compose 中配置。
5. 启动 FastAPI
dockerfile
CMD ["fastapi", "run", "app/main.py", "--port", "8000", "--proxy-headers", "--forwarded-allow-ips=*"]
建议将 CMD 写成完整的一行。
如果直接拆成下面这种格式:
dockerfile
CMD [
"fastapi",
"run"
]
Dockerfile 没有正确添加续行符时,可能会报错:
text
unknown instruction: "fastapi"
五、配置 Docker Compose
创建 compose.yaml:
yaml
services:
api:
build:
context: .
dockerfile: Dockerfile
args:
PIP_INDEX_URL: https://pypi.tuna.tsinghua.edu.cn/simple
image: order-api:1.0.0
container_name: order-api
restart: unless-stopped
env_file:
- .env
ports:
- "127.0.0.1:18000:8000"
healthcheck:
test:
[
"CMD",
"python",
"-c",
"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=5)"
]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
这里最重要的是下面这段:
yaml
ports:
- "127.0.0.1:18000:8000"
Docker 端口映射格式:
text
宿主机IP:宿主机端口:容器端口
因此,这条配置的含义是:
text
127.0.0.1 : 18000 : 8000
↓ ↓ ↓
宿主机IP 宿主机端口 容器端口
最终访问关系:
text
http://127.0.0.1:18000
↓
Docker容器中的8000端口
六、为什么使用 127.0.0.1
本文使用:
yaml
ports:
- "127.0.0.1:18000:8000"
这表示 18000 端口只允许宿主机本机访问。
局域网和公网中的其他电脑不能直接访问:
text
http://服务器IP:18000
这种方式适合生产环境,因为 FastAPI 不会直接暴露到公网,外部请求统一经过 Nginx。
如果确实需要让其他电脑直接访问,可以修改为:
yaml
ports:
- "18000:8000"
它等价于:
yaml
ports:
- "0.0.0.0:18000:8000"
其他电脑便可以访问:
text
http://宿主机IP:18000
但正式环境不建议这样配置。
七、配置环境变量
创建 .env:
dotenv
APP_ENV=production
如果项目中使用数据库和 Redis,可以继续添加:
dotenv
DATABASE_URL=mysql+pymysql://user:password@database-host:3306/order_db
REDIS_URL=redis://redis-host:6379/0
API_TOKEN=replace-with-real-token
不要将 .env 提交到 Git。
建议在 .gitignore 中添加:
gitignore
.env
.venv/
__pycache__/
*.pyc
.pytest_cache/
八、配置 Docker 忽略文件
创建 .dockerignore:
text
.git
.env
.venv
__pycache__
.pytest_cache
*.pyc
tests
这样可以避免无关文件和敏感信息进入 Docker 镜像。
九、构建 Docker 镜像
执行:
bash
docker compose build --progress=plain
--progress=plain 可以显示完整的构建日志。
如果需要忽略缓存,重新构建:
bash
docker compose build --no-cache --progress=plain
如果构建成功,可以查看镜像:
bash
docker images
预期可以看到:
text
order-api 1.0.0
十、启动容器
执行:
bash
docker compose up -d
如果修改过 Dockerfile 或端口映射,建议执行:
bash
docker compose up -d --build --force-recreate
查看运行状态:
bash
docker compose ps
正常情况下会看到:
text
NAME STATUS PORTS
order-api Up (healthy) 127.0.0.1:18000->8000/tcp
其中:
text
127.0.0.1:18000->8000/tcp
说明宿主机的 18000 已经成功映射到容器的 8000。
十一、验证接口
1. 查看端口映射
bash
docker port order-api
预期:
text
8000/tcp -> 127.0.0.1:18000
2. 测试健康检查
bash
curl http://127.0.0.1:18000/health
预期:
json
{"status":"ok"}
3. 访问 Swagger
浏览器打开:
text
http://127.0.0.1:18000/docs
4. 测试业务接口
bash
curl -X POST 'http://127.0.0.1:18000/api/v1/orders' \
-H 'Content-Type: application/json' \
-d '{
"customer_id": "C10001",
"amount": 99.5
}'
预期:
json
{
"code": "0",
"message": "success",
"data": {
"customer_id": "C10001",
"amount": 99.5
}
}
十二、修改宿主机映射端口
假设需要将宿主机端口从 18000 改成 9000:
yaml
ports:
- "127.0.0.1:9000:8000"
重新创建容器:
bash
docker compose up -d --force-recreate
新访问地址:
bash
curl http://127.0.0.1:9000/health
这里修改的是宿主机端口,容器中的 FastAPI 仍然使用 8000。
十三、配置 Nginx 反向代理
安装 Nginx:
bash
sudo apt update
sudo apt install nginx
创建配置文件:
text
/etc/nginx/sites-available/order-api
配置如下:
nginx
server {
listen 80;
listen [::]:80;
server_name api.example.com;
client_max_body_size 10m;
access_log /var/log/nginx/order-api-access.log;
error_log /var/log/nginx/order-api-error.log;
location / {
proxy_pass http://127.0.0.1:18000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Request-Id $request_id;
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
重点是:
nginx
proxy_pass http://127.0.0.1:18000;
因为 Nginx运行在宿主机上,所以需要访问宿主机映射后的 18000 端口。
启用配置:
bash
sudo ln -s /etc/nginx/sites-available/order-api \
/etc/nginx/sites-enabled/order-api
检查 Nginx 配置:
bash
sudo nginx -t
重新加载:
bash
sudo systemctl reload nginx
测试:
bash
curl http://api.example.com/health
十四、配置 HTTPS
安装 Certbot:
bash
sudo apt install certbot python3-certbot-nginx
申请证书:
bash
sudo certbot --nginx -d api.example.com
测试 HTTPS:
bash
curl https://api.example.com/health
检查证书自动续期:
bash
sudo certbot renew --dry-run
配置完成后,业务方使用:
text
https://api.example.com
不需要知道 Docker 内部端口或宿主机的 18000 端口。
十五、不使用 Docker Compose 的写法
如果不使用 Docker Compose,可以通过 docker run 映射端口:
bash
docker run -d \
--name order-api \
--restart unless-stopped \
-p 127.0.0.1:18000:8000 \
order-api:1.0.0
其中:
text
-p 127.0.0.1:18000:8000
与 Compose 中的配置完全相同:
yaml
ports:
- "127.0.0.1:18000:8000"
踩坑记录
问题一:Docker Hub连接超时
错误信息:
text
Client.Timeout exceeded while awaiting headers
或者:
text
failed to resolve source metadata for docker.io/library/python:3.12-slim
这说明 Docker 无法正常访问:
text
https://registry-1.docker.io/v2/
可以尝试:
- 为 Docker Desktop 配置代理;
- 使用公司内部 Harbor;
- 使用可访问的公共镜像仓库;
- 在其他机器下载后离线导入。
本文使用:
dockerfile
FROM public.ecr.aws/docker/library/python:3.12-slim
作为替代。
问题二:pip install 十几分钟没有完成
如果构建长时间停留在:
text
RUN pip install ...
一般是容器访问 PyPI 速度较慢或直接超时。
使用下面的命令可以查看详细日志:
bash
docker compose build --no-cache --progress=plain
如果日志中反复出现:
text
Retrying
ReadTimeoutError
Connection timed out
可以配置:
dockerfile
ARG PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
如果通过本机代理构建,假设代理端口是 7890:
bash
docker compose build \
--build-arg HTTP_PROXY=http://host.docker.internal:7890 \
--build-arg HTTPS_PROXY=http://host.docker.internal:7890 \
--no-cache \
--progress=plain
Mac Docker Desktop 中,容器访问宿主机需要使用:
text
host.docker.internal
不能使用:
text
127.0.0.1
因为容器中的 127.0.0.1 指向容器自身。
问题三:Dockerfile 出现 unknown instruction
错误信息:
text
unknown instruction: "fastapi"
原因通常是 CMD 被错误拆行。
推荐写法:
dockerfile
CMD ["fastapi", "run", "app/main.py", "--port", "8000", "--proxy-headers", "--forwarded-allow-ips=*"]
问题四:宿主机端口被占用
错误信息:
text
port is already allocated
Linux 查看端口:
bash
sudo ss -lntp | grep 18000
Mac 查看端口:
bash
lsof -nP -iTCP:18000 -sTCP:LISTEN
解决方法:
- 停止占用端口的进程;
- 或者更换宿主机端口。
例如:
yaml
ports:
- "127.0.0.1:18001:8000"
问题五:修改端口后没有生效
Docker 容器创建后,端口映射不能直接动态修改。
修改 compose.yaml 后,需要重新创建容器:
bash
docker compose up -d --force-recreate
问题六:健康检查失败
健康检查运行在容器内部,因此应该访问:
text
http://127.0.0.1:8000/health
不能写成:
text
http://127.0.0.1:18000/health
18000 是宿主机端口,8000 才是容器内部端口。
十六、常用命令汇总
启动服务:
bash
docker compose up -d
停止并删除容器:
bash
docker compose down
重新构建:
bash
docker compose build --no-cache --progress=plain
重新创建容器:
bash
docker compose up -d --build --force-recreate
查看状态:
bash
docker compose ps
查看日志:
bash
docker compose logs -f api
查看端口映射:
bash
docker port order-api
测试服务:
bash
curl http://127.0.0.1:18000/health
总结
本次部署最关键的是理解下面三个端口之间的关系:
text
Nginx 80/443
↓
宿主机 127.0.0.1:18000
↓
Docker 容器 8000
↓
FastAPI
Dockerfile 中定义 FastAPI 使用的容器端口:
dockerfile
EXPOSE 8000
Docker Compose 完成端口映射:
yaml
ports:
- "127.0.0.1:18000:8000"
Nginx访问映射后的宿主机端口:
nginx
proxy_pass http://127.0.0.1:18000;
本机测试地址:
text
http://127.0.0.1:18000/health
最终提供给业务方的地址:
text
https://api.example.com
使用这种部署方式,可以避免直接把 FastAPI 容器端口暴露到公网,同时保留 Docker 部署、升级和回滚的便利性。
文章标签:
text
Python、FastAPI、Docker、Docker Compose、Nginx、接口部署、端口映射