第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 的选项、批量流水线的写法,以及校验失败时的定位方法。