为什么 Alembic 在生产环境正常,却没有为 Docker 中的数据库建表?
结论:正常退出不代表执行了建表操作
现有日志不足以确定唯一原因。应先核对 env.py 的连接来源,确认 Alembic 实际连接的数据库,再对比容器内的 heads、history 和 current 输出,检查可见的迁移和数据库记录的当前 revision。这些信息有助于区分连接错误、迁移文件缺失和版本状态不一致。
alembic upgrade head 会执行已有迁移文件中尚未应用的 upgrade(),不会自动根据 SQLAlchemy 或 SQLModel 模型生成表结构。如果没有可执行的迁移、版本已经到达 head,或者连接到了另一个数据库,都可能出现退出码为 0、目标库却没有业务表的情况。
日志中有哪些线索?
迁移服务完成依赖安装后,只出现了:
INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO [alembic.runtime.migration] Will assume transactional DDL.
migrate-1 exited with code 0
这些日志说明 Alembic 配置了 PostgreSQL 迁移上下文,进程没有报告错误。提供的日志中没有出现常见的 Running upgrade ... 记录,不能仅凭这些信息认定建表迁移已经执行。
PostgreSQL 日志中还有一条记录:
PostgreSQL Database directory appears to contain a database; Skipping initialization
这说明 postgres_data 中已有数据库数据,本次启动复用了原来的数据库状态。它不能说明业务表存在,也不能说明 Alembic 的版本记录正确。
pg_isready 检查数据库是否接受连接。condition: service_healthy 可以控制迁移服务的启动时机,但不会检查表结构是否完整。
1. 确认 Alembic 使用了 Compose 中的连接地址
配置中设置了:
TRAVELER_DB_HOST: postgresql+psycopg://traveler:traveler@postgres:5432/traveler
这个地址适用于同一 Compose 网络内的 migrate 服务。Alembic 不会自动识别名为 TRAVELER_DB_HOST 的环境变量,必须由项目配置代码或 env.py 读取它。
检查 traveler/db/alembic.ini 和实际使用的 env.py,重点确认:
- 连接最终来自
sqlalchemy.url,还是项目自己的配置对象。 TRAVELER_DB_HOST在代码中代表完整 URL,还是仅代表主机名。当前传入的是完整 URL。- 是否存在默认连接地址或其他配置覆盖了这个环境变量。
如果项目决定让该变量保存完整 URL,同步迁移的连接部分可以写成下面这样。示例中的 target_metadata 需要沿用项目实际配置,这段代码不能直接作为完整的 env.py 使用:
import os
from alembic import context
from sqlalchemy import create_engine, pool
def run_migrations_online():
engine = create_engine(
os.environ["TRAVELER_DB_HOST"],
poolclass=pool.NullPool,
)
with engine.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
)
with context.begin_transaction():
context.run_migrations()
同时确认在线执行分支确实调用了 run_migrations_online()。如果现有 env.py 已有正确的连接和事务逻辑,只修正配置来源即可。
从宿主机上的数据库客户端连接时,使用映射端口对应的 localhost:5432;从 migrate 容器连接时,使用服务名 postgres:5432。两端都应选择数据库 traveler。
2. 在迁移容器环境中检查版本和迁移文件
migrate 已退出,可以用一次性容器执行检查。下面的命令保留原有的依赖安装步骤,并覆盖服务启动命令:
docker compose run --rm migrate bash -lc '
pip install --no-cache-dir -e . &&
alembic -c traveler/db/alembic.ini heads --verbose &&
alembic -c traveler/db/alembic.ini history --verbose &&
alembic -c traveler/db/alembic.ini current --verbose
'
这三个命令分别用于检查:
| 命令 | 检查内容 |
|---|---|
heads --verbose | 当前可见迁移脚本的末端 revision |
history --verbose | 迁移链及脚本位置 |
current --verbose | 按当前连接配置读取的数据库 revision |
根据结果继续判断:
heads和history没有列出迁移:检查script_location、可能配置的version_locations,以及对应文件在/app中是否存在。./:/app会挂载宿主机目录,该目录必须包含迁移脚本。- 数据库当前 revision 已等于 head:Alembic 不会重复执行旧迁移。需要继续确认是否连错数据库、查看了错误的 schema,或曾经只标记版本而没有执行 DDL。
- 数据库没有当前 revision,但有完整迁移链:确认连接目标后执行升级,并检查是否出现实际迁移记录。若仍无操作,应检查
env.py是否调用了context.run_migrations()。
current 查询的也是 Alembic 自己配置的数据库,因此必须结合前一步的连接核对结果一起看。
3. 检查建表操作是否存在于迁移脚本中
找到迁移文件,查看其中的 upgrade()。负责建立初始结构的迁移应包含相应的 op.create_table(...),或其他明确执行建表的操作。如果迁移函数只有 pass,即使升级成功也不会创建业务表。
如果项目还没有初始建表迁移,应先确保模型已注册到 metadata,再针对确认无误的开发数据库生成迁移:
alembic -c traveler/db/alembic.ini revision --autogenerate -m "create initial tables"
对于 SQLModel 项目,通常需要设置:
from sqlmodel import SQLModel
# 设置前,需要导入定义数据库表的模型模块。
target_metadata = SQLModel.metadata
只导入 SQLModel 不会自动加载项目中的所有模型。如果表模型模块没有被导入,metadata 可能缺少表定义,自动生成的迁移也可能不符合预期。
生成后必须检查 upgrade() 和 downgrade(),确认包含预期的建表操作、没有意外的删除操作,再执行:
alembic -c traveler/db/alembic.ini upgrade head
如果生产环境已经有完整迁移历史,应优先找回并使用这套迁移文件,避免另建一套互不衔接的初始迁移。自动生成迁移属于开发步骤,不宜放进每次容器启动流程。
4. 直接检查容器数据库中的表和 schema
从 PostgreSQL 服务内部连接,可以先排除数据库客户端连错实例的问题:
docker compose exec postgres psql -U traveler -d traveler
然后执行:
SELECT current_database(), current_user, current_schema();
SHOW search_path;
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
ORDER BY table_schema, table_name;
这可以帮助确认业务表是否位于客户端未展开的其他 schema 中,以及是否存在 alembic_version 表。如果使用默认版本表名且它位于 public,再查询:
SELECT version_num FROM public.alembic_version;
如果配置了自定义版本表名或 version_table_schema,应按实际配置查询。
假如版本表已经记录 head,但预期业务表不存在,需要调查是否执行过 stamp head、手动删表,或者应用过空迁移。stamp 只修改迁移版本记录,不会执行建表操作,不能用它修复缺失的表。
5. 核实持久化数据卷的历史状态
当前数据库使用命名卷:
volumes:
- postgres_data:/var/lib/postgresql/data
重启或重建容器会继续使用卷中的数据库。对于已有数据目录,修改 POSTGRES_DB、POSTGRES_USER 或 POSTGRES_PASSWORD 不会重新初始化数据库。
确认开发数据库中的数据可以丢弃后,可以重建对应数据卷,再用完整迁移链初始化。需要保留数据时,应备份并制定修复迁移。不要一开始就删除卷,这会同时丢失用于排查的版本记录和现有数据。
题目中的 Compose 代码缩进看起来在粘贴时丢失了,实际 YAML 需要正确缩进。不过日志已经显示两个服务都被启动,不能仅凭展示格式认定这就是本次故障原因。
备注:内容仅供参考。
