云端部署

Docker:FastAPI 与 PostgreSQL 应用容器化部署

把一个 FastAPI 记账接口和 PostgreSQL 数据库打包成可以直接上线的 Docker 应用:多阶段构建镜像、非 root 用户运行、健康检查、Compose 编排、密码放在 .env 里,再加一个自动备份和恢复数据库的脚本。

BINBHO · 图文教程

把一个 FastAPI 记账接口和 PostgreSQL 数据库打包成可以直接上线的 Docker 应用:多阶段构建镜像、非 root 用户运行、健康检查、Compose 编排、密码放在 .env 里,再加一个自动备份和恢复数据库的脚本。

先看清成品

成品是一个项目文件夹,在任何装了 Docker 的机器上执行 docker compose up -d --build,就会启动 API 和数据库两个容器,API 等数据库健康后才启动。本文全部步骤已在 Linux 上实际运行:两个容器均显示 healthy,接口正确返回记账汇总,镜像大小 182 MB,容器内用户为 appuser,备份后删除数据卷、再恢复,数据完整回来。

做完之后你会得到什么?

成果 说明
多阶段 Dockerfile 构建依赖不进入最终镜像
Compose 编排 API + PostgreSQL 17,带健康检查和启动顺序
密钥管理 数据库密码放 .env,不进代码仓库
记账 API 新增支出、查询汇总、健康检查
备份脚本 导出压缩备份,保留最近 7 份

需要准备哪些环境?

项目 要求 说明
Docker Engine 或 Docker Desktop 支持 docker compose docker compose version 能输出版本
系统 Linux、macOS、Windows 均可 上线服务器推荐 Linux
端口 8000 空闲 被占用时改 Compose 的端口映射

第一步:项目结构是什么样的?

ledger/
├── app/
│   ├── __init__.py
│   └── main.py
├── requirements.txt
├── Dockerfile
├── .dockerignore
├── compose.yaml
├── .env.example      # 提交到仓库的模板
├── .env              # 真实密码,不提交
└── backup.sh

1. 让 AI 生成初稿(可选)

帮我把一个 FastAPI + PostgreSQL 项目容器化:
- Dockerfile 用多阶段构建,基础镜像 python:3.12-slim
- 最终镜像以非 root 用户运行,带 HEALTHCHECK 调用 /healthz
- compose.yaml 包含 api 和 db(postgres:17),db 带 pg_isready 健康检查,api 等 db 健康后启动
- 数据库密码从 .env 读取
- 再写一个 pg_dump 备份脚本,保留最近 7 份

第二步:应用代码怎么写?

1. app/main.py

import os
from contextlib import asynccontextmanager

import psycopg
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

DB = os.environ["DATABASE_URL"]


def conn():
    return psycopg.connect(DB, autocommit=True)


@asynccontextmanager
async def lifespan(app):
    with conn() as c:
        c.execute("""CREATE TABLE IF NOT EXISTS expense (
            id serial PRIMARY KEY, item text NOT NULL, amount numeric(10,2) NOT NULL,
            created_at timestamptz DEFAULT now())""")
    yield


app = FastAPI(title="记账 API", lifespan=lifespan)


class ExpenseIn(BaseModel):
    item: str
    amount: float


@app.get("/healthz")
def healthz():
    try:
        with conn() as c:
            c.execute("SELECT 1")
    except psycopg.OperationalError:
        raise HTTPException(503, "database unavailable")
    return {"ok": True}


@app.post("/expenses", status_code=201)
def add(e: ExpenseIn):
    with conn() as c:
        r = c.execute("INSERT INTO expense (item, amount) VALUES (%s, %s) RETURNING id", (e.item, e.amount)).fetchone()
    return {"id": r[0]}


@app.get("/expenses/summary")
def summary():
    with conn() as c:
        n, total = c.execute("SELECT count(*), coalesce(sum(amount), 0) FROM expense").fetchone()
    return {"count": n, "total": float(total)}

/healthz 会真的查询一次数据库,数据库挂了就返回 503,这样健康检查才有意义。

2. requirements.txt

fastapi==0.156.4
uvicorn[standard]==0.54.0
psycopg[binary]==3.3.6

版本号写死,保证每次构建结果一致。

第三步:Dockerfile 怎么写?

1. Dockerfile

# syntax=docker/dockerfile:1

# ---------- 第一阶段:安装依赖 ----------
FROM python:3.12-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# ---------- 第二阶段:运行 ----------
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
RUN useradd --create-home --uid 10001 appuser
WORKDIR /app
COPY --from=builder /install /usr/local
COPY app ./app
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s --retries=3 \
  CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/healthz').status == 200 else 1)"
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
写法 原因
两个 FROM 第一阶段装依赖,第二阶段只复制安装结果
先 COPY requirements.txt 再装依赖 改代码时依赖层可以复用缓存,构建更快
useradd + USER appuser 容器被攻破时权限更小
HEALTHCHECK 用 Python 标准库 slim 镜像里没有 curl,不用额外安装
PYTHONUNBUFFERED=1 日志实时输出,docker logs 立刻能看到

2. .dockerignore

.git
.venv
__pycache__
*.pyc
.env

.env 写进 .dockerignore,防止密码被打包进镜像。

第四步:Compose 怎么编排?

1. compose.yaml

services:
  api:
    build: .
    image: ledger-api:1.0
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgresql://ledger:${DB_PASSWORD}@db:5432/ledger
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:17
    environment:
      POSTGRES_USER: ledger
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ledger
    volumes:
      - dbdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ledger -d ledger"]
      interval: 5s
      timeout: 3s
      retries: 10
    restart: unless-stopped

volumes:
  dbdata:

2. 生成密码

cp .env.example .env
echo "DB_PASSWORD=$(openssl rand -hex 16)" > .env

注意:把 .env 加进 .gitignore,只提交 .env.example。

3. 构建并启动

docker compose up -d --build
docker compose ps

本文实际状态:

api Up 20 seconds (healthy)
db Up 25 seconds (healthy)

4. 调用接口

curl -s -X POST localhost:8000/expenses -H 'content-type: application/json' -d '{"item":"午饭","amount":32.5}'
curl -s -X POST localhost:8000/expenses -H 'content-type: application/json' -d '{"item":"地铁","amount":4}'
curl -s localhost:8000/expenses/summary

本文实际输出:

{"id":1}{"id":2}
{"count":2,"total":36.5}

5. 检查结果

检查项 命令 本文结果
镜像大小 docker images ledger-api:1.0 182 MB
运行用户 docker compose exec api id uid=10001(appuser)
接口文档 浏览器打开 localhost:8000/docs 自动生成的 Swagger 页面

第五步:怎样备份和恢复数据库?

1. backup.sh

#!/usr/bin/env bash
# 每天备份一次数据库,保留最近 7 份
set -euo pipefail
cd "$(dirname "$0")"
mkdir -p backups
f="backups/ledger-$(date +%Y%m%d-%H%M%S).sql.gz"
docker compose exec -T db pg_dump -U ledger ledger | gzip > "$f"
ls -1t backups/*.sql.gz | tail -n +8 | xargs -r rm --
echo "已备份到 $f"
./backup.sh

本文实际输出:

已备份到 backups/ledger-20261021-131036.sql.gz

2. 定时备份

crontab -e

加入一行,每天凌晨 3 点备份:

0 3 * * * /home/deploy/ledger/backup.sh >> /home/deploy/ledger/backups/backup.log 2>&1

3. 恢复演练

不演练过的备份不算备份。本文实际做了一次:删除数据卷后重建,再导入备份。

docker compose down -v
docker compose up -d db
zcat backups/ledger-20261021-131036.sql.gz | docker compose exec -T db psql -U ledger ledger
docker compose up -d
curl -s localhost:8000/expenses/summary

恢复后汇总重新显示 {"count":2,"total":36.5}。

先只启动 db 再导入,可以避免 API 启动时抢先建表,导入时报“表已存在”。

注意:down -v 会删除数据卷里的全部数据,只在演练或确认有备份时使用。备份文件要复制到另一台机器或云存储,和服务器放在一起等于没备份。

出问题时该怎么调?

1. api 一直是 starting 或 unhealthy

docker compose logs api --tail 50
docker inspect --format '{{json .State.Health}}' $(docker compose ps -q api)

第二条命令能看到健康检查每次的输出。常见原因是 DATABASE_URL 密码和数据库不一致。

2. 改了 .env 里的密码后连不上

PostgreSQL 只在数据卷为空时使用 POSTGRES_PASSWORD 初始化。已有数据时,要么进数据库修改密码,要么备份后删除数据卷重建。

3. 构建时 pip 下载失败

报错含 Temporary failure in name resolution 时,是构建容器无法联网。检查 Docker 的 DNS 和代理设置,可以先用 docker run --rm busybox nslookup pypi.org 测试。

4. KeyError: 'DATABASE_URL'

直接在本机运行了 uvicorn,而没有设置环境变量。本地调试时先设置:

export DATABASE_URL=postgresql://ledger:密码@localhost:5432/ledger

并在 Compose 里给 db 临时加上端口映射 5432:5432。

常见问题怎么处理?

现象 原因 解决方法
port is already allocated 8000 被占用 改成 "8080:8000"
改代码后没变化 没重新构建 docker compose up -d --build
镜像很大 用了完整版 python 镜像 用 slim 并保持多阶段
备份文件为 0 字节 db 容器没运行 先 docker compose ps 确认
permission denied 写文件 非 root 用户无写权限 只给需要写的目录授权,不要改回 root
服务器重启后服务没起来 Docker 未设开机启动 sudo systemctl enable docker

项目资源

用途 地址
Dockerfile 参考 https://docs.docker.com/reference/dockerfile/
多阶段构建 https://docs.docker.com/build/building/multi-stage/
Compose 文件参考 https://docs.docker.com/reference/compose-file/
Compose 启动顺序 https://docs.docker.com/compose/how-tos/startup-order/
PostgreSQL 镜像 https://hub.docker.com/_/postgres
pg_dump 文档 https://www.postgresql.org/docs/current/app-pgdump.html
FastAPI 部署到 Docker https://fastapi.tiangolo.com/deployment/docker/
Psycopg 3 文档 https://www.psycopg.org/psycopg3/docs/

建议先只做到第四步,在自己电脑上看到两个容器都是 healthy,再用 curl 调一次接口。

然后务必做一次第五步的恢复演练,确认备份真的能恢复,再把项目部署到服务器上。