尚拙

一个分享技术、学习成长的个人博客网站

0%

Docker部署FastAPI项目实践

最近我用 FastAPI 写了一个后端项目,项目使用 PostgreSQL 作为数据库,数据库访问采用异步方式,ORM 使用 SQLAlchemy,数据库迁移使用 Alembic。项目在本地开发完成后,我开始尝试把它部署到云服务器上。

这是我第一次正式使用 Docker 部署项目。和之前直接在服务器上安装 Python、创建虚拟环境、安装依赖、手动启动服务不同,这次我选择把 FastAPI 应用打包成 Docker 镜像,然后在云服务器上通过 Docker 容器运行。

不过需要说明的是,这次部署中 数据库没有使用 Docker 部署。PostgreSQL 是独立部署的,可能是云服务器上单独安装的 PostgreSQL,也可能是云厂商提供的云数据库服务。Docker 只负责运行 FastAPI 应用本身。

这篇文章主要记录我第一次使用 Docker 部署 FastAPI 项目的完整过程,包括 Dockerfile 编写、环境变量配置、连接外部 PostgreSQL、执行 Alembic 数据库迁移、云服务器部署、日志查看,以及第一次使用 Docker 时遇到的一些问题。


一、为什么只把应用放进 Docker

刚开始接触 Docker 时,我以为所有服务都应该放进容器,包括应用、数据库、缓存等。后来实际部署时发现,并不是所有东西都必须容器化。

这次我的选择是:

FastAPI 应用:使用 Docker 部署
PostgreSQL 数据库:独立部署,不放进 Docker

这样做有几个原因。

首先,数据库是有状态服务,最重要的是数据安全和持久化。虽然 PostgreSQL 也可以放进 Docker,并通过 volume 持久化数据,但对于第一次部署来说,我更希望数据库保持独立,避免因为容器、volume 或部署脚本操作不当影响数据。

其次,如果使用云数据库,数据库本身已经由云厂商负责运行、备份、监控和高可用,应用只需要通过连接地址访问它即可。这种方式比自己维护数据库容器更加省心。

最后,把应用容器化已经可以解决很多部署问题,比如 Python 版本、依赖版本、运行环境不一致等。数据库独立出来之后,整体架构也更清晰。

最终的部署结构大概是这样:

用户请求

云服务器 / 反向代理

Docker 容器中的 FastAPI 应用

外部 PostgreSQL 数据库

二、项目部署前的目录结构

我的项目大致结构如下:

fastapi-demo/
├── alembic/
│ ├── versions/
│ └── env.py
├── app/
│ ├── api/
│ ├── core/
│ ├── db/
│ ├── models/
│ ├── schemas/
│ ├── services/
│ └── main.py
├── alembic.ini
├── Dockerfile
├── docker-compose.yml
├── .dockerignore
├── .env
└── requirements.txt

其中,app/main.py 是 FastAPI 应用入口,alembic 目录用于数据库迁移,requirements.txt 用来记录 Python 项目依赖,Dockerfile 用于构建应用镜像,docker-compose.yml 用于在服务器上管理应用容器。

因为数据库没有使用 Docker,所以这次的 docker-compose.yml 里只需要配置 FastAPI 应用服务,不需要再配置 PostgreSQL 服务。


三、准备 requirements.txt

首先需要确认项目依赖都写在 requirements.txt 中。

示例:

fastapi
uvicorn
sqlalchemy
asyncpg
alembic
pydantic
pydantic-settings
python-multipart

如果项目中使用了密码加密、JWT 或其他功能,也需要把对应依赖加入进去:

passlib[bcrypt]
python-jose

第一次使用 Docker 部署时,我发现一个很容易忽略的问题:本地虚拟环境里装了某个包,但忘记写进 requirements.txt。本地运行没问题,一到容器里就报错:

ModuleNotFoundError: No module named 'xxx'

原因很简单,Docker 构建镜像时只会安装 requirements.txt 中声明的依赖。本地虚拟环境里有什么包,容器并不知道。

所以部署前一定要确认依赖文件完整。


四、编写 Dockerfile

在项目根目录创建 Dockerfile

FROM python:3.12-slim

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

这个 Dockerfile 的作用是把 FastAPI 项目打包成一个可以运行的镜像。

FROM python:3.12-slim 表示使用 Python 3.12 的轻量版镜像作为基础环境。WORKDIR /app 表示容器内的工作目录是 /app

我先复制 requirements.txt 并安装依赖,然后再复制项目代码。这是一个比较常见的写法,因为 Docker 构建镜像时有缓存机制。只要依赖文件没有变化,下次构建时就不需要重复安装依赖,可以节省构建时间。

最后通过 uvicorn 启动 FastAPI 应用。

这里特别需要注意:

--host 0.0.0.0

在 Docker 容器中启动 Web 服务时,不能只监听 127.0.0.1。如果只监听容器内部的本地地址,宿主机或者外部网络可能访问不到服务。监听 0.0.0.0 表示允许容器内所有网络接口访问这个服务。


五、编写 .dockerignore

在项目根目录创建 .dockerignore

.venv
__pycache__
*.pyc
.git
.gitignore
.env
.pytest_cache
.mypy_cache
.idea
.vscode

.dockerignore 的作用是告诉 Docker,在构建镜像时哪些文件不需要复制进去。

这里我特意把 .env 加进了忽略列表。因为 .env 通常包含数据库连接地址、数据库密码、密钥等敏感信息,不应该直接打包进镜像。

更推荐的做法是:镜像只包含代码和依赖,具体环境配置在容器运行时注入。

这样同一个镜像可以用于开发环境、测试环境和生产环境,只需要提供不同的环境变量即可。


六、配置外部 PostgreSQL 数据库连接

因为数据库没有使用 Docker,所以 .env 文件里的数据库地址应该指向外部 PostgreSQL,而不是 docker-compose 里的服务名。

如果 PostgreSQL 安装在同一台云服务器上,并且监听本机地址,可能是:

DATABASE_URL=postgresql+asyncpg://db_user:db_password@127.0.0.1:5432/db_name

但是这里要注意一个非常关键的问题:容器里的 127.0.0.1 不是云服务器的 127.0.0.1,而是容器自己的 127.0.0.1

也就是说,如果 FastAPI 跑在 Docker 容器里,而 PostgreSQL 跑在宿主机上,那么容器内访问:

127.0.0.1:5432

访问到的是容器自己,不是宿主机上的 PostgreSQL。

所以如果数据库安装在宿主机上,需要根据实际环境选择合适的连接方式。

一种方式是让 PostgreSQL 监听服务器内网 IP,然后在 .env 中使用服务器内网 IP:

DATABASE_URL=postgresql+asyncpg://db_user:db_password@服务器内网IP:5432/db_name

如果使用的是云数据库,则一般直接使用云数据库提供的连接地址:

DATABASE_URL=postgresql+asyncpg://db_user:db_password@数据库连接地址:5432/db_name

比如:

DATABASE_URL=postgresql+asyncpg://fastapi_user:strong_password@postgres.example.internal:5432/fastapi_demo

需要注意,异步 SQLAlchemy 连接 PostgreSQL 时,协议前缀应该使用:

postgresql+asyncpg://

而不是:

postgresql://

否则可能无法正常使用异步数据库连接。


七、编写 docker-compose.yml

因为数据库不在 Docker 中,所以 docker-compose.yml 只需要定义 FastAPI 应用服务。

示例:

services:
web:
build: .
container_name: fastapi_web
command: uvicorn app.main:app --host 0.0.0.0 --port 8000
ports:
- "8000:8000"
env_file:
- .env
restart: always

这个配置比应用和数据库都容器化的版本简单很多。

build: . 表示使用当前目录下的 Dockerfile 构建镜像。ports 表示把容器内部的 8000 端口映射到服务器的 8000 端口。env_file 表示从 .env 文件中读取环境变量。restart: always 表示容器异常退出或服务器重启后,Docker 会尝试自动重启服务。

如果后面使用 Nginx 做反向代理,并且不想直接把 8000 端口暴露到公网,可以改成:

services:
web:
build: .
container_name: fastapi_web
command: uvicorn app.main:app --host 0.0.0.0 --port 8000
ports:
- "127.0.0.1:8000:8000"
env_file:
- .env
restart: always

这样 FastAPI 只暴露给服务器本机,公网用户不能直接访问 服务器IP:8000,只能通过 Nginx 或其他反向代理访问。


八、本地测试 Docker 构建

在部署到云服务器之前,最好先在本地测试 Docker 镜像是否能正常构建。

执行:

docker compose up -d --build

查看容器状态:

docker compose ps

查看应用日志:

docker compose logs -f web

如果容器正常运行,可以访问:

http://127.0.0.1:8000/docs

查看 FastAPI 自动生成的接口文档。

不过需要注意,如果本地容器要连接远程 PostgreSQL,需要确保本地网络可以访问数据库,并且数据库白名单、安全组、防火墙都允许当前机器连接。

如果本地无法访问生产数据库,也可以在本地单独准备一套测试数据库连接配置。


九、部署到云服务器

本地测试没问题后,就可以部署到云服务器。

首先登录服务器:

ssh user@server_ip

进入项目目录:

mkdir -p /opt/fastapi-demo
cd /opt/fastapi-demo

拉取项目代码:

git clone your_repository_url .

如果项目已经存在,后续更新可以执行:

git pull

然后确认服务器上已经安装 Docker 和 Docker Compose:

docker --version
docker compose version

在服务器项目目录中准备 .env 文件:

DATABASE_URL=postgresql+asyncpg://db_user:db_password@数据库地址:5432/db_name

这里的数据库地址要根据实际情况填写。

如果使用云数据库,填写云数据库的内网地址或公网地址。

如果数据库安装在同一台服务器上,不要想当然地在容器里写 127.0.0.1。需要确认容器能否访问宿主机的 PostgreSQL。更推荐使用服务器内网 IP,并确保 PostgreSQL 监听地址、防火墙和数据库权限都配置正确。

启动服务:

docker compose up -d --build

查看容器状态:

docker compose ps

查看日志:

docker compose logs -f web

如果日志里没有报错,说明 FastAPI 应用已经在容器中启动。


十、执行 Alembic 数据库迁移

项目使用 Alembic 管理数据库表结构。因为数据库独立部署,所以迁移命令本质上是:在应用容器中执行 Alembic,然后连接外部 PostgreSQL,完成表结构更新。

执行:

docker compose exec web alembic upgrade head

这条命令表示进入 web 容器,并执行:

alembic upgrade head

Alembic 会读取容器中的环境变量,也就是 .env 中的 DATABASE_URL,然后连接外部 PostgreSQL 执行迁移。

查看当前迁移版本:

docker compose exec web alembic current

如果修改了 ORM 模型,需要先生成迁移文件:

alembic revision --autogenerate -m "add new field"

然后提交迁移文件,部署到服务器后执行:

docker compose exec web alembic upgrade head

我个人更推荐在本地或测试环境生成迁移文件,并认真检查迁移内容,确认无误后再部署到生产环境执行。

不要在生产服务器上随意使用 --autogenerate 生成迁移文件。生产环境应该只执行已经确认过的迁移脚本。


十一、配置反向代理

如果只是测试,可以直接开放 8000 端口访问 FastAPI:

http://服务器IP:8000/docs

但正式部署时,更推荐使用 Nginx 做反向代理。

比如 Nginx 配置可以写成:

server {
listen 80;
server_name your_domain.com;

location / {
proxy_pass http://127.0.0.1:8000;
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;
}
}

使用反向代理后,可以把 docker-compose.yml 的端口映射改成:

ports:
- "127.0.0.1:8000:8000"

这样应用容器只对服务器本机开放端口,外部请求统一从 Nginx 进入。

这种方式比直接暴露应用端口更适合正式部署,也方便后续配置 HTTPS、域名、访问日志和限流。


十二、常用 Docker 命令记录

启动服务:

docker compose up -d

重新构建并启动:

docker compose up -d --build

停止服务:

docker compose down

查看容器状态:

docker compose ps

查看应用日志:

docker compose logs -f web

进入应用容器:

docker compose exec web sh

如果镜像里安装了 bash,也可以使用:

docker compose exec web bash

执行数据库迁移:

docker compose exec web alembic upgrade head

重启应用容器:

docker compose restart web

查看所有容器:

docker ps -a

查看所有镜像:

docker images

清理无用资源:

docker system prune

清理命令执行前要谨慎确认,避免误删仍然需要的镜像、容器或缓存。


十三、第一次部署时遇到的问题

1. 容器里的 localhost 不是服务器 localhost

这是这次部署中最容易踩的坑。

因为数据库没有使用 Docker,而是独立部署,所以一开始很容易把数据库地址写成:

DATABASE_URL=postgresql+asyncpg://db_user:db_password@127.0.0.1:5432/db_name

但是 FastAPI 运行在 Docker 容器里。对容器来说,127.0.0.1 指的是容器自己,不是宿主机。

所以如果 PostgreSQL 跑在宿主机上,容器内访问 127.0.0.1:5432 通常是连不上的。

解决方式是使用宿主机可被容器访问的 IP 地址,或者使用云数据库提供的连接地址。


2. 数据库安全组和白名单没有放行

如果使用云数据库,除了数据库地址、用户名和密码正确之外,还需要检查云数据库的安全组、白名单或访问控制规则。

应用容器运行在云服务器上,本质上是从云服务器发起数据库连接。所以数据库需要允许这台云服务器访问。

如果网络不通,应用日志里可能会出现连接超时、连接拒绝等错误。


3. PostgreSQL 没有监听正确地址

如果 PostgreSQL 安装在服务器宿主机上,还需要确认 PostgreSQL 是否允许外部连接。

有时 PostgreSQL 只监听:

127.0.0.1

这种情况下,容器通过服务器内网 IP 访问可能会失败。

需要检查 PostgreSQL 的监听地址、访问权限和防火墙配置。


4. 忘记使用 asyncpg 连接前缀

因为项目使用的是异步 SQLAlchemy,所以数据库连接地址应该使用:

postgresql+asyncpg://

如果写成普通的:

postgresql://

可能会导致异步数据库连接无法正常工作。


5. 忘记执行 Alembic 迁移

容器启动成功不代表数据库表已经存在。

如果数据库是新的,或者代码中新增了表和字段,就需要执行:

docker compose exec web alembic upgrade head

否则接口访问数据库时可能会报表不存在、字段不存在等错误。


十四、一次完整的部署流程

最终,我整理出来的部署流程是这样的:

本地完成 FastAPI 项目

确认 requirements.txt

编写 Dockerfile

编写 docker-compose.yml

准备 .env 数据库连接配置

本地构建镜像并测试

提交代码

登录云服务器

拉取最新代码

在服务器上配置 .env

docker compose up -d --build

docker compose exec web alembic upgrade head

查看应用日志

配置反向代理

项目上线

用流程图表示:

flowchart TD
A[本地完成 FastAPI 项目] --> B[整理 requirements.txt]
B --> C[编写 Dockerfile]
C --> D[编写 docker-compose.yml]
D --> E[配置外部 PostgreSQL 连接]
E --> F[本地 Docker 构建测试]
F --> G[提交代码]
G --> H[登录云服务器]
H --> I[拉取最新代码]
I --> J[准备服务器 .env 文件]
J --> K[构建并启动应用容器]
K --> L[执行 Alembic 数据库迁移]
L --> M[查看日志和接口状态]
M --> N[配置 Nginx 反向代理]
N --> O[项目上线]

十五、最终 docker-compose.yml 示例

因为数据库没有使用 Docker,所以最终的 docker-compose.yml 很简单:

services:
web:
build: .
container_name: fastapi_web
command: uvicorn app.main:app --host 0.0.0.0 --port 8000
ports:
- "127.0.0.1:8000:8000"
env_file:
- .env
restart: always

如果暂时没有配置 Nginx,需要直接通过服务器 IP 和端口访问,可以改成:

ports:
- "8000:8000"

正式部署时,我更推荐使用:

ports:
- "127.0.0.1:8000:8000"

然后通过 Nginx 转发外部请求。


十六、这次 Docker 部署的收获

这次部署让我对 Docker 有了更实际的理解。

Docker 并不意味着一定要把所有服务都放进容器。对于后端项目来说,完全可以只把应用层容器化,而数据库使用独立部署或云数据库。

这种方式既保留了 Docker 带来的环境一致性,也降低了数据库运维复杂度。

通过 Dockerfile,我可以把 FastAPI 应用的运行环境固定下来。通过 docker-compose,我可以用统一的命令启动、停止、重启应用。通过 .env,我可以把敏感配置从镜像中剥离出来。通过 Alembic,我可以在容器中对外部 PostgreSQL 执行数据库迁移。

这次最大的收获是理解了容器网络和数据库连接的关系。尤其是容器里的 localhost 并不是宿主机的 localhost,这一点如果不理解,部署时很容易卡在数据库连接问题上。

整体来看,这种部署方式非常适合第一次把 FastAPI 项目上线:应用使用 Docker,数据库保持独立,部署流程清晰,后续也方便继续扩展到 Nginx、HTTPS、自动化部署、日志采集和监控。