第04章 - 开发环境与工具链搭建
这套工具链的安装难点不在命令数量,而在版本咬合:GDAL 与 rasterio 的 ABI 要对上,Python 版本要满足 rio-cogeo 7.x 与 stac-fastapi 7.x 的 ≥ 3.11,pgstac 的 schema 版本要落在 stac-fastapi-pgstac 声明的区间里,PostGIS 要在 pgstac 之前装好。任何一处不匹配,报错信息通常指向别的地方,排查成本很高。
这一章给一套能一次装成的最小组合,以及三个可验证的检查点。装完之后,第 5 章起的所有示例都能直接跑。
4.1 版本矩阵
先定组合,再动手。下表是本教程验证过的版本,同一行内的组件互相兼容。
| 组件 | 版本 | 安装方式 | 说明 |
|---|---|---|---|
| Python | 3.11 / 3.12 | 系统或 conda | rio-cogeo 与 stac-fastapi 都要求 ≥ 3.11 |
| GDAL | 3.13.3 | conda-forge 或系统包 | ≥ 3.1 才有 COG driver 与 ghost 区 |
| rasterio | 随 GDAL 匹配 | conda-forge 或 wheel | 不要混装 pip 与 conda 版本 |
| rio-cogeo | 7.0.4 | pip | 依赖 rasterio ≥ 1.3.3、morecantile ≥ 5 |
| pystac | 1.16.0 | pip | 校验需 pystac[validation] |
| stac-fastapi | 7.1.1 | pip | 拆成 api/types/extensions 三个包 |
| stac-fastapi-pgstac | 7.2.1 | pip | 要求 pgstac ≥ 0.9.2,< 0.11 |
| pgstac | 0.10.1 | Docker 镜像或 SQL | 需要 PostGIS |
| PostgreSQL | 16 / 17 | Docker | pgstac 用 LIST + RANGE 分区 |
| PostGIS | 3.4+ | 随 Postgres 镜像 | 必须早于 pgstac 安装 |
| Node.js | 20 LTS | nvm 或系统包 | 仅 STAC Browser 需要 |
| STAC Browser | 5.2.0 | 源码构建或 Docker | 构建时决定 historyMode |
两个硬约束值得单独强调:
- rio-cogeo 7.x 的 pyproject 写的是 requires-python = “>=3.11”。Python 3.10 会在 pip 解析阶段就失败,报 “Could not find a version that satisfies the requirement”。
- stac-fastapi-pgstac 的版本表:>= 7.1 对应 pgstac >= 0.9.2,< 0.11。装成 pgstac 0.8 会在启动时因为函数签名不匹配而报错,且错误信息是 PL/pgSQL 的 “function search(jsonb) does not exist”,看起来像代码问题。
4.2 三条安装路线
4.2.1 路线一:conda(推荐做 COG 生产)
GDAL 的 C 依赖复杂,conda-forge 的二进制包最省事,且 rasterio、pyproj、fiona 会自动跟着 GDAL 版本走。
# 建环境
conda create -n stac python=3.12 -y
conda activate stac
# GDAL 与 rasterio 从 conda-forge 装,保证 ABI 一致
conda install -c conda-forge gdal=3.13 rasterio pyproj -y
# 纯 Python 包用 pip
pip install "rio-cogeo==7.0.4" "pystac[validation]==1.16.0"
# 验证
gdalinfo --version
# GDAL 3.13.3, released 2025/xx/xx
rio --version
# rio 1.5.x
rio cogeo --help | head -5
conda 路线的坑是混装:先 conda 装了 GDAL,再用 pip 装一个带二进制的 rasterio,两者链接的 libgdal 版本不同,运行时报 “PROJ: proj_create_from_database: Cannot find proj.db” 或者段错误。判断方法:
python -c "import rasterio; print(rasterio.__gdal_version__)"
gdalinfo --version
两个版本号必须一致。不一致就卸掉 pip 版重装。
4.2.2 路线二:纯 pip(推荐做服务端)
服务端只需要读 COG,不需要写,可以用 manylinux 的 wheel:
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install --upgrade pip
pip install "rio-cogeo==7.0.4" "pystac[validation]==1.16.0"
pip install "stac-fastapi.api==7.1.1" "stac-fastapi.types==7.1.1" \
"stac-fastapi.extensions==7.1.1"
pip install "stac-fastapi.pgstac==7.2.1"
rasterio 的 wheel 自带 GDAL 动态库,所以不需要系统装 GDAL。代价是版本固定,不能用系统 GDAL 的新特性(比如某些压缩算法)。
检查 wheel 自带的 GDAL 版本:
python -c "import rasterio; print(rasterio.__gdal_version__, rasterio.__version__)"
# 3.9.x 1.4.x
4.2.3 路线三:Docker(推荐做集成环境)
要同时跑 Postgres(pgstac 作为其中的 schema)与 MinIO,docker compose 最省心。stac-fastapi 用 pip 装在本机跑,不放进 compose——它需要频繁改代码与重启,容器化反而拖慢迭代。下面这份 compose 是本章后续示例的基础:
# docker-compose.yml
services:
postgres:
image: postgis/postgis:16-3.4
environment:
POSTGRES_USER: pgstac
POSTGRES_PASSWORD: pgstac
POSTGRES_DB: pgstac
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U pgstac"]
interval: 5s
retries: 10
# pgstac 不单独起服务:它是 Postgres 里的 schema,
# 由下面 4.3 节的 pgstac-migrate 迁移到 postgres 容器中。
minio:
image: minio/minio:latest
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
ports:
- "9000:9000"
- "9001:9001"
volumes:
- miniodata:/data
volumes:
pgdata:
miniodata:
docker compose up -d
docker compose ps
# NAME IMAGE STATUS
# postgres postgis/postgis:16-3.4 Up (healthy)
# minio minio/minio:latest Up
pgstac 只是 Postgres 里的 schema 与函数,不需要单独容器。0.10 把迁移拆成独立的 pgstac-migrate 包(Python CLI),从宿主机连到容器里的库执行:
python -m pip install pgstac-migrate
pgstac-migrate migrate --dsn "postgresql://pgstac:pgstac@localhost:5432/pgstac"
# 确认版本
psql "postgresql://pgstac:pgstac@localhost:5432/pgstac" \
-c "SELECT pgstac.get_version();"
4.3 三个检查点
装完必须验证,否则后面报错时无法区分“环境问题”还是“代码问题”。
4.3.1 检查点一:GDAL 能开远程 COG
export GDAL_DISABLE_READDIR_ON_OPEN=EMPTY_DIR
export CPL_VSIL_CURL_ALLOWED_EXTENSIONS=.tif
gdalinfo /vsicurl/https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-2-l2a-assets/2023/5/1/S2B_10TFK_20230501_0_L2A/B04.tif | head -20
能打印出 Size、Block、Overviews 就说明 /vsicurl/ 通路正常。两个环境变量很关键:
- GDAL_DISABLE_READDIR_ON_OPEN=EMPTY_DIR:禁止 GDAL 去列目录找 .ovr/.aux.xml。对对象存储,列目录意味着一次 LIST 请求,慢且可能没权限。
- CPL_VSIL_CURL_ALLOWED_EXTENSIONS=.tif:只允许探测 .tif 后缀的附属文件,进一步减少无谓请求。
不加这两个变量时,打开一个 COG 可能发 4–6 次请求而不是 1 次。
4.3.2 检查点二:rio-cogeo 能生成并校验
# 造一个测试影像
gdal_create -of GTiff -outsize 4096 4096 -bands 3 -burn 120 \
-ot Byte -a_srs EPSG:32650 -a_ullr 500000 4400000 622840 4277160 \
/tmp/src.tif
# 转成 COG
rio cogeo create /tmp/src.tif /tmp/dst.tif --cog-profile deflate --blocksize 512
# 校验
rio cogeo validate --strict /tmp/dst.tif
# /tmp/dst.tif is a valid cloud optimized GeoTIFF
如果 validate 报 “The file is greater than 512xH or 512xW, but is not tiled”,说明 –blocksize 没生效——通常是 profile 里 blockxsize 被 creation options 覆盖了。
4.3.3 检查点三:stac-fastapi 能起来
export STAC_FASTAPI_TITLE="我的影像目录"
export STAC_FASTAPI_DESCRIPTION="示例 STAC API"
export STAC_FASTAPI_VERSION="1.0.0"
export STAC_FASTAPI_LANDING_ID="my-catalog"
uvicorn stac_fastapi.pgstac.app:create_app --factory --port 8082
curl -s http://localhost:8082/ | python -m json.tool | head -20
# {
# "type": "Catalog",
# "stac_version": "1.1.0",
# "id": "my-catalog",
# "title": "我的影像目录",
# "conformsTo": [
# "https://api.stacspec.org/v1.0.0/core",
# ...
# ]
# }
注意 create_app 要用 –factory 参数。直接写模块路径会报 “AttributeError: module has no attribute app”,因为 7.x 只提供工厂函数,不提供模块级 app 变量。
4.4 国内网络的三个卡点
4.4.1 conda 与 pip 源
# pip 走国内镜像
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip config set install.trusted-host pypi.tuna.tsinghua.edu.cn
# conda 走清华源(写入 ~/.condarc)
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge
conda config --set show_channel_urls yes
注意 conda-forge 的镜像同步有延迟,装最新版 GDAL 时可能找不到。这时临时用官方源:
conda install -c conda-forge --override-channels gdal=3.13 -y
4.4.2 Docker 镜像
ghcr.io 在国内经常拉不动。可用的替代:
# 用镜像加速站(示例,按实际可用性选择)
docker pull docker.m.daocloud.io/ghcr.io/stac-utils/pgstac:v0.10.1
docker tag docker.m.daocloud.io/ghcr.io/stac-utils/pgstac:v0.10.1 \
ghcr.io/stac-utils/pgstac:v0.10.1
更稳的做法是在有外网的机器上 docker save / docker load:
# 有外网的机器
docker save ghcr.io/stac-utils/pgstac:v0.10.1 -o pgstac.tar
# 内网机器
docker load -i pgstac.tar
4.4.3 示例数据
公开的 COG 样例大多在 us-west-2,国内访问延迟 200–400 ms。做功能验证没问题,做性能测试必须换成自己的对象存储。
# 下载一个小样例到本地,避免反复走公网
curl -L -o sample.tif \
"https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-2-l2a-assets/2023/5/1/S2B_10TFK_20230501_0_L2A/B04.tif"
ls -lh sample.tif
# -rw-r--r-- 1 user user 12M ... sample.tif
4.5 MinIO:本地对象存储
开发阶段用 MinIO 替代 S3,API 兼容,行为接近。三步配好:
# 1. 建桶
docker run --rm --network host --entrypoint /bin/sh minio/mc -c "
mc alias set local http://localhost:9000 minioadmin minioadmin &&
mc mb local/imagery &&
mc anonymous set download local/imagery
"
# 2. 上传一个 COG
docker run --rm --network host -v /tmp:/tmp --entrypoint /bin/sh minio/mc -c "
mc cp /tmp/dst.tif local/imagery/demo/dst.tif
"
# 3. 验证 Range 请求
curl -s -D - -o /dev/null -r 0-99 http://localhost:9000/imagery/demo/dst.tif
# HTTP/1.1 206 Partial Content
# Accept-Ranges: bytes
# Content-Range: bytes 0-99/12345678
# Content-Length: 100
看到 206 与 Accept-Ranges: bytes 就说明 Range 通路正常。如果返回 200,说明桶策略或代理不支持 Range——第 6 章会专门讲这个。
MinIO 与 S3 的一个行为差异:MinIO 默认支持多 range 请求(multipart/byteranges),S3 不支持。所以本地测试通过的“一次请求取两段”逻辑,上 S3 会退化成两次请求。写代码时不要依赖多 range。
4.6 环境变量清单
stac-fastapi 的配置全部走环境变量(pydantic-settings,大小写不敏感)。常用的:
| 变量 | 默认值 | 说明 |
|---|---|---|
| STAC_FASTAPI_TITLE | stac-fastapi | 落地页标题 |
| STAC_FASTAPI_DESCRIPTION | stac-fastapi | 落地页描述 |
| STAC_FASTAPI_VERSION | 0.1 | API 版本 |
| STAC_FASTAPI_LANDING_ID | stac-fastapi | 落地页 id |
| ENABLE_RESPONSE_MODELS | false | 是否用 pydantic 校验输出 |
| ENABLE_DIRECT_RESPONSE | false | 跳过序列化直接返回 Response |
| OPENAPI_URL | /api | OpenAPI 文档路径 |
| DOCS_URL | /api.html | Swagger UI 路径 |
| ROOT_PATH | 空 | 反向代理前缀 |
pgstac 后端的额外变量:
| 变量 | 默认值 | 说明 |
|---|---|---|
| PGUSER / PGPASSWORD / PGHOST / PGPORT / PGDATABASE | 无(必填) | 数据库连接 |
| DB_MIN_CONN_SIZE | 1 | 连接池下限 |
| DB_MAX_CONN_SIZE | 10 | 连接池上限 |
| DB_MAX_QUERIES | 50000 | 单连接最大查询数,之后重建 |
| DB_MAX_INACTIVE_CONN_LIFETIME | 300 | 空闲连接存活秒数 |
| ENABLED_EXTENSIONS | 空(=全部) | 逗号分隔的扩展列表 |
| ENABLE_TRANSACTIONS_EXTENSIONS | false | 开启写接口 |
| USE_API_HYDRATE | false | 在 API 层做 hydration |
| PREFIX_PATH | 空 | 路由前缀 |
ENABLED_EXTENSIONS 的可选值来自扩展映射表的键:query、sort、fields、filter、pagination、free_text、collection_search、extra。不设这个变量等于全开——生产上建议显式列出来,避免升级后新扩展悄悄上线。
两个 flag 互斥:ENABLE_RESPONSE_MODELS 与 ENABLE_DIRECT_RESPONSE 同时为 true 会在启动时抛 ValueError:
ValueError: `enable_reponse_models` and `enable_direct_response` options are incompatible
注意报错里的 enable_reponse_models 是源码里的拼写错误(少了一个 s),搜这个字符串能定位到 ApiSettings 的 model_validator。
4.7 排错
| 报错 | 原因 | 修法 |
|---|---|---|
| PROJ: proj_create_from_database: Cannot find proj.db | GDAL 与 pyproj 的 PROJ 数据目录不匹配 | 统一用 conda 装,或设 PROJ_LIB |
| ImportError: libgdal.so.32: cannot open shared object file | 混装了 pip 与 conda 的 GDAL | 卸掉 pip 版 rasterio |
| Could not find a version that satisfies rio-cogeo | Python < 3.11 | 升 Python |
| function search(jsonb) does not exist | pgstac 版本过旧 | 升到 0.10.x 并跑迁移 |
| psycopg.OperationalError: connection refused | compose 里 postgres 还没 healthy | 加 depends_on condition |
| uvicorn 报 module has no attribute app | 没用 –factory | 加 –factory |
| MinIO 返回 200 而不是 206 | 桶策略或 mc 版本问题 | 用 curl -r 验证,检查匿名读策略 |
第一条在国内环境特别常见:conda 装的 GDAL 用自己那份 proj.db,pip 装的 pyproj 又带一份,两者版本不同。诊断:
python -c "import pyproj; print(pyproj.datadir.get_data_dir())"
gdal-config --datadir 2>/dev/null || echo "no gdal-config"
两个路径应该一致,或者至少 PROJ 版本一致。
4.8 生产建议
开发用 compose,生产用分层镜像。 开发时一个 compose 起全套最省事;生产上把 stac-fastapi 与 pgstac 分开部署,数据库用托管服务,镜像里只装运行期依赖(不带 gcc、不带测试包)。
把 GDAL 配置固化进镜像的 ENV。 这些变量对性能的影响比代码优化大:
ENV GDAL_DISABLE_READDIR_ON_OPEN=EMPTY_DIR \
CPL_VSIL_CURL_ALLOWED_EXTENSIONS=.tif \
GDAL_HTTP_MAX_RETRY=3 \
GDAL_HTTP_RETRY_DELAY=1 \
VSI_CACHE=TRUE \
VSI_CACHE_SIZE=33554432
VSI_CACHE_SIZE 是 32 MB。GDAL 的 /vsicurl/ 还有一层 16 MB 的全局 LRU(CPL_VSIL_CURL_CACHE_SIZE),两层缓存叠加时注意别把内存算少了。
锁定版本,不要用 latest。 这套工具链的组件之间有明确的版本区间,latest 会在某次升级后突然不兼容。生产镜像用精确 tag,requirements 用 ==。
给 CI 一个环境自检脚本。 每次构建跑一遍,比线上发现问题便宜得多:
#!/usr/bin/env bash
set -euo pipefail
python -c "import sys; assert sys.version_info >= (3, 11), sys.version"
python -c "import rasterio; print('rasterio', rasterio.__version__, 'gdal', rasterio.__gdal_version__)"
rio cogeo --help > /dev/null
python -c "import pystac; print('pystac', pystac.__version__)"
python -c "import stac_fastapi.api; print('stac-fastapi ok')"
echo "environment OK"
4.9 本章小结
| 要点 | 结论 |
|---|---|
| Python | 必须 ≥ 3.11(rio-cogeo 7.x 与 stac-fastapi 7.x 的共同要求) |
| GDAL | ≥ 3.1;conda 装最稳,pip wheel 自带库但版本固定 |
| 版本咬合 | stac-fastapi-pgstac ≥ 7.1 对应 pgstac ≥ 0.9.2,< 0.11 |
| 三个检查点 | /vsicurl/ 能开远程 COG、rio cogeo validate 通过、uvicorn 能起 |
| 关键环境变量 | GDAL_DISABLE_READDIR_ON_OPEN、CPL_VSIL_CURL_ALLOWED_EXTENSIONS |
| 本地对象存储 | MinIO 支持多 range,S3 不支持,代码不要依赖多 range |
下一章进入 COG 生产:rio-cogeo 的 create 参数、GDAL COG driver 的选项、批量流水线的写法,以及校验失败时的定位方法。