资讯动态

Docker Compose部署PostGIS全攻略:从基础环境到生产调优

发布时间:2026/10/21 8:02:00 来源:尧图企业网站定制
最近有个业务系统要上地理围栏功能需要在PostgreSQL里存经纬度轨迹、计算点与多边形的包含关系。环境是Docker Swarm集群隔壁团队用apt直接把PostGIS装在了宿主机上结果升级PostgreSQL小版本的时候把空间扩展搞挂了数据差点没救回来。我这边图省事直接走的docker-compose编排半小时跑完一套带PostGIS的空间数据库环境。这篇就聊聊我实际怎么装、怎么配、踩了哪些坑尽量把关键点都摊开说清楚。1. 为什么用docker-compose装PostGIS而不是直接装系统包先说我自己的处境。开发机是Ubuntu生产是CentOS中间还有同事的macOS每个人本机系统都不一样。如果每个人都去宿主机装PostgreSQL和PostGIS版本不一致、依赖冲突、初始化方式五花八门联调一遍能把人折腾疯。docker-compose的好处就是把“数据库服务”当成一个整体单元来管理镜像版本、端口映射、数据目录、初始化脚本全部写在一个yaml里任何机器上跑起来都是一模一样的环境。另一个现实原因是PostGIS本身依赖较多。它在PostgreSQL之上加了一层空间数据类型、空间索引和空间函数系统包管理器装的时候要处理GEOS、Proj、GDAL等一系列原生库的版本匹配。我踩过最疼的一个坑是用apt装PostgreSQL 13的库再去装对应版本的postgis包结果因为Proj版本太新导致ST_Transform算出来的坐标偏移了十几米排查了大半天才发现是底层库不兼容。Docker镜像把这套原生依赖在构建时锁死了我用到的postgis/postgis:14-3.4镜像里GEOS和Proj版本是经过官方测试的不会再出现这种底层库冲突。还有一点很实际docker-compose可以配合命名卷保存数据。PostgreSQL容器随便删掉重建只要卷还在数据就不丢。这比在宿主机上直接dpkg安装要干净得多想回滚就把镜像tag换回去再起一个容器一分钟的事。提示如果你只是本机临时玩一下用docker run单容器跑也没问题。但只要涉及环境共享、多人协作、或者有部署脚本需求建议直接上docker-compose后面维护省很多事。2. 镜像选型postgis/postgis官方镜像与postgres官方镜像的版本对应关系Docker Hub上有几个PostGIS镜像可选最推荐的是postgis/postgis。这个镜像是PostGIS官方维护的标签规则是PostgreSQL大版本-PostGIS大版本.小版本比如14-3.4就是PostgreSQL 14配合PostGIS 3.415-3.4就是PostgreSQL 15配合PostGIS 3.4。选择依据我个人看三点和现有系统的兼容性。如果能接受新版本尽量选PostgreSQL 15或16搭配PostGIS 3.4以上。如果公司内部还有老系统依赖,那就跟着能用就行别追新。镜像大小和启动速度。postgis/postgis镜像比纯postgres镜像大不少因为多了GDAL和Proj这些原生库大概多出300MB左右拉取时间要有点耐心。是否内置PostGIS的PostgreSQL扩展包。postgis/postgis镜像的好处是已经装好了PostGIS所有扩展你只需要在数据库里执行CREATE EXTENSION就行不用自己下载安装包。我生产环境用的就是这个组合services: db: image: postgis/postgis:15-3.4 container_name: gis-db如果要更细致地管理版本可以在.env文件里定义镜像版本避免在yaml里到处硬编码# .env POSTGIS_IMAGEpostgis/postgis:15-3.4再在docker-compose.yml里引用services: db: image: ${POSTGIS_IMAGE}这样升级镜像版本时只改一行环境变量下次部署生效也方便用git管理版本变化。3. 编写docker-compose.yml端口、卷、健康检查、资源限制的配置逻辑先给出一份能直接跑的完整配置然后逐个字段说明为什么这么写version: 3.8 services: db: image: postgis/postgis:15-3.4 container_name: my-postgis restart: always environment: POSTGRES_USER: gis_user # 超级用户名别用默认的postgres降低被爆破风险 POSTGRES_PASSWORD: ${DB_PASSWORD:-change_me} POSTGRES_DB: gis_db # 初始数据库名 TZ: Asia/Shanghai # 时区否则容器默认UTC日志时间看着很别扭 PGDATA: /var/lib/postgresql/data/pgdata ports: - 5432:5432 volumes: - gis-data:/var/lib/postgresql/data - ./init-scripts:/docker-entrypoint-initdb.d:ro healthcheck: test: [CMD-SHELL, pg_isready -U gis_user -d gis_db] interval: 10s timeout: 5s retries: 5 start_period: 10s shm_size: 1gb deploy: resources: limits: memory: 2g cpus: 2.0 volumes: gis-data: driver: local端口映射那块。左边是宿主机端口右边是容器内PostgreSQL默认的5432端口。如果宿主机上已经有别的PostgreSQL在跑左边就换成5433或者其他端口避免冲突。我实际遇到过把宿主机上的监控脚本连错端口的情况这里改清楚能少很多麻烦。卷挂载是最关键的部分。gis-data:/var/lib/postgresql/data这个语法把PostgreSQL的实际数据目录持久化到了Docker命名卷里。注意这里不能挂到宿主机普通目录比如./data尤其别用网络文件系统NFSPostgreSQL对磁盘IO和文件锁很敏感NFS挂载容易出数据损坏。我见过有人图方便把数据目录映射到本机/srv/postgres/data结果文件权限和SELinux上下文各种问题最后容器起不来。初始化脚本目录。./init-scripts:/docker-entrypoint-initdb.d会把宿主机上init-scripts目录下的.sh、.sql、.sql.gz文件按文件名顺序执行。这个机制在容器第一次启动创建数据目录时有效执行完之后再重启容器不会重复执行。我习惯在这里放数据库初始化SQL比如创建普通业务账号、建扩展、设置权限等。注意docker-entrypoint-initdb.d里的脚本只在数据卷为空时执行。如果你的数据卷已经有数据改脚本不会生效需要把卷删掉重新初始化或者手动进容器执行。这个坑很多新手踩删卷前务必确认数据备份好了。健康检查。我用了pg_isready命令它会检查PostgreSQL是否接受连接。注意执行时指定了用户-U gis_user和数据库-d gis_db这样可以确保不仅是数据库进程活着而且目标数据库和用户都可用。start_period: 10s表示容器启动后10秒内不算失败给足初始化时间。我之前没写健康检查编排里有别的服务等待数据库就绪结果数据库还在初始化的时候应用就连接了报了一堆连接拒绝错误。加上这个之后依赖服务可以等db健康后再启动。shm_size设置。PostgreSQL的并行查询和PostGIS的空间索引构建会用到共享内存Docker默认的/dev/shm只有64MB容易触发问题。设置shm_size: 1gb是比较稳妥的只影响容器内/dev/shm虚拟文件系统不占宿主机实际内存。资源限制。deploy.resources.limits限制容器最多使用2GB内存和2个CPU。在Compose里这个字段对docker-compose和Docker Swarm都生效使用docker compose v2时也能识别。我建议线上一定要限制不然数据库跑一个聚合查询就能把整个宿主机的CPU吃满其他容器全部受到牵连。环境变量里配置的POSTGRES_USER、POSTGRES_PASSWORD、POSTGRES_DB会被官方镜像的entrypoint脚本读取用于初始化超级用户“gis_user”、数据库“gis_db”。密码我用了.env文件里的DB_PASSWORD避免明文写在编排文件里提交到代码仓库。4. 初始化工作如何在首次启动时自动创建PostGIS空间扩展官方镜像在入口脚本里做了个额外动作如果发现数据库初始化目录里有.sh或.sql文件就会在创建数据库后执行这些文件。所以我的初始化脚本第一行就创建空间扩展。-- init-scripts/01-enable-postgis.sql CREATE EXTENSION IF NOT EXISTS postgis; CREATE EXTENSION IF NOT EXISTS postgis_topology; CREATE EXTENSION IF NOT EXISTS postgis_raster;扩展加不加取决于你的业务需求我拆开说扩展名功能使用场景postgis核心空间数据类型的扩展包含geometry、geography类型空间函数GiST空间索引必装postgis_topology拓扑数据处理扩展用于处理节点、边、面构成的高级空间拓扑结构复杂空间分析可能会用postgis_raster栅格数据支持用于处理遥感影像、DEM高程等涉及影像分析才需要postgis_sfcgal基于CGAL库的高级几何函数如缓冲区、多边形化、三角剖分等复杂几何操作可选装postgis_tiger_geocoder美国地址地理编码工具国内业务基本不需要很多人以为只装postgis就完了其实PostGIS是一组扩展的集合。比如我要用postgis_topology做路网分析没装这个扩展就去调用函数会直接报function xx does not exist然后你翻遍PostGIS官网文档也不知道问题出在哪因为文档默认你知道要单独启用对应扩展。我还建议在同一个SQL文件里设置Search Path。PostGIS的几何类型默认在public模式如果你的业务数据在别的模式下面每次查表都要写public.geometry太烦可以这样-- init-scripts/02-set-search-path.sql ALTER DATABASE gis_db SET search_path TO public, gis, postgis;如果你还要在PostGIS里面使用地理坐标系转换建议加上坐标参考系相关查询验证-- 验证PostGIS安装 SELECT postgis_full_version();这条SQL会返回PostGIS完整版本号和依赖的GEOS、Proj、GDAL版本信息方便排查后续底层库问题。假设你还需要在空间数据库里存中文地址建议初始化时设置-- init-scripts/03-chinese-support.sql CREATE EXTENSION IF NOT EXISTS pg_trgm;pg_trgm做模糊搜索很有用但很多初学者不知道PostGIS要和它搭配使用尤其是文本地址模糊匹配的时候用普通LIKE可能走不上索引但pg_trgm可以建立GIN索引加速。注意以上SQL文件的执行顺序按照文件名升序排列。用“01-”“02-”“03-”这样的前缀可以明确控制执行顺序这在我的项目里是一个约定团队里大家都这么写。5. 生产环境的关键调优内存、连接数、WAL参数与空间索引维护5.1 调整shared_buffers和work_memPostGIS的空间计算特别吃内存。比如一个几十万点的几何数据表做缓冲区计算内存不够就直接落盘速度慢到怀疑人生。我最开始用的默认配置跑一次ST_Intersects关联查询花了4分钟。调整参数之后降到20秒左右。在容器里临时调参可以进容器执行docker exec -it my-postgis psql -U gis_user -d gis_db -c ALTER SYSTEM SET shared_buffers 1GB;但容器重建后配置丢失更正规的做法是把参数写在compose的command字段里command: - postgres - -c - shared_buffers1GB - -c - work_mem64MB - -c - maintenance_work_mem256MB - -c - max_connections100官方镜像的entrypoint会用postgres加这些参数启动数据库注意postgres是最后那个二进制文件名不是占位符。参数含义参数推荐值说明shared_buffers物理内存的25%不超过2GBPostgreSQL的共享缓冲池所有查询共享work_mem查询排序、哈希等操作的内存预算可以设置得大一些但注意每个排序都会用这么多设置太大容易撑爆空间索引构建特别吃这个maintenance_work_mem自动清理、创建索引时的内存建空间索引时用到的比普通查询多建议至少256MBmax_connections100配合连接池使用别把数值拉太高每个连接都会吃内存5.2 WAL归档与备份设置生产环境必须考虑数据安全。PostgreSQL默认配置下WAL文件只有一小段崩溃恢复能力有限。我给生产库加了最小化的归档配置command: - postgres - -c - wal_levelreplica - -c - archive_modeon - -c - archive_command/bin/truearchive_command那里在生产环境要改成实际备份命令比如把WAL文件拷贝到远程存储。我这里先用/bin/true占位让归档模式开着但不产生实际归档行为至少保证后续接备份方案时不需重启数据库。5.3 空间索引与清理维护PostGIS的GiST空间索引并不是建一次就万事大吉。业务数据持续增删改之后索引会逐渐膨胀查询性能下降。我的经验是定期执行REINDEX INDEX idx_geom_geo; VACUUM ANALYZE;如果表特别大REINDEX会锁表尽量放在业务低峰期执行。日常运维里我用pg_cron扩展或者宿主机crontab定时跑。也可以用autovacuum来管理数据库自身的行版本清理但空间索引的膨胀autovacuum解决不了必须手动REINDEX或者用pg_repack来重排。另外PostGIS 3.0以上的版本支持用ST_Subdivide把大几何对象切成小块再建索引处理超大地物的查询效率能提高不少。这个函数在安装PostGIS扩展后就有不需要额外启用。5.4 坐标系选择与SRID建议空间数据入库前一定要统一坐标系。我建议业务中使用WGS84经纬度EPSG:4326存储因为前端地图瓦片、GPS设备输出都是这个坐标系。但做距离计算和面积计算时4326是角度坐标系算出来是弧度/平方度没有任何直观意义。真要算距离用ST_Transform转到EPSG:3857墨卡托或者用适合小范围的局部投影坐标系。举个例子计算两个POI距离SELECT ST_Distance( ST_Transform(ST_SetSRID(ST_MakePoint(120.15, 30.28), 4326), 3857), ST_Transform(ST_SetSRID(ST_MakePoint(120.16, 30.29), 4326), 3857) ) / 1000 AS distance_km;这么写能拿到大概距离但3857在高纬度地区的变形明显如果要精确计算应该用geography类型SELECT ST_Distance( ST_SetSRID(ST_MakePoint(120.15, 30.28), 4326)::geography, ST_SetSRID(ST_MakePoint(120.16, 30.29), 4326)::geography ) / 1000 AS distance_km;用geography类型会使用球面距离计算精度比3857高。但geography的函数支持比geometry少一些计算成本也高一些。我的原则是存储用geometry4326需要距离时用::geography即时转换既保证存储效率又拿到正确距离。6. 实测过程中遇到的坑容器重建丢数据、扩展初始化失败、时区不对6.1 容器重建后数据没了的根因排查有一次我改docker-compose.yml里的映射端口执行了docker-compose up -d重建容器。一开始一切正常但跑了一会儿发现业务数据全没了表里面干干净净。我第一反应是卷没挂好检查docker volume ls发现gis-data卷还在数据不可能丢。结果登录数据库一查发现容器初始化脚本又执行了一遍把数据库重置了。原因出在我把./init-scripts写成了绑定挂载而数据卷挂载点写错了位置。之前一直用/var/lib/postgresql/data但某个版本改成了PGDATA环境变量指定的/var/lib/postgresql/data/pgdata官方镜像的entrypoint检测到PGDATA变化后会在新路径初始化数据旧路径下的卷变成了“悬空数据”等于数据库从头来了一遍。解法是显式把卷挂到PGDATA对应的真实路径environment: PGDATA: /var/lib/postgresql/data/pgdata volumes: - gis-data:/var/lib/postgresql/data/pgdata或者干脆不要设置PGDATA变量保持默认卷直接挂/var/lib/postgresql/data。我最终选择了后者简单直接官方镜像的默认行为最稳。6.2 初始化SQL执行报错却不知道原因我一开始在init-scripts里放了一个建表SQL里面引用了postgis扩展。结果容器启动时报错说extension postgis does not exist。我检查了镜像标签确实是postgis/postgis:15-3.4百思不得解。后来拉镜像的Dockerfile看了一下发现官方镜像的entrypoint虽然内置了PostGIS扩展文件但不会自动在数据库里执行CREATE EXTENSION。装扩展文件不等于启用扩展。你必须在初始化脚本里显式创建扩展。而且官方镜像还有个默认行为它在初始化时会创建一个名为template_postgis的模板数据库里面已经装好了PostGIS扩展。你可以直接用postgis模板创建新数据库避免每次建库都要重新跑脚本。但用docker-entrypoint-initdb.d初始化时默认操作的是POSTGRES_DB指定的库扩展创建要自己写。我改成了更可靠的方式初始化脚本里先判断是否存在再创建DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname postgis) THEN CREATE EXTENSION postgis; END IF; END $$但坦白说CREATE EXTENSION IF NOT EXISTS已经够用不需要这种防御式写法前提是脚本别重复执行。docker-entrypoint-initdb.d的脚本只在数据目录为空时执行一次重复执行的概率很低。6.3 时区问题容器默认UTC导致数据时间偏移8小时PostgreSQL容器默认时区是UTC。如果你的业务在北京时间那么now()返回的时间和本地时间差了8小时。我最早没注意前端页面显示的时间老是慢8小时排查了半天发现数据库日志的时间也对不上。解决方式有两个。一是在environment里设置TZ: Asia/Shanghai让容器内系统时区改到东八区。二是在PostgreSQL里设置ALTER DATABASE gis_db SET timezone TO Asia/Shanghai;建议两个都做一个是操作系统层一个是数据库层。只改container的TZPostgreSQL内部时区不一定变只改数据库时区系统日志的时间还是UTC。6.4 镜像拉取超时的应对措施有些网络环境拉取Docker Hub官方镜像很慢。postgis/postgis镜像有几个G的分层拉取超时挺常见的。我用的方案是配置镜像加速器在/etc/docker/daemon.json里写{ registry-mirrors: [https://docker.m.daocloud.io] }然后重启docker服务。这个地址是我本地实测可用的但各地区情况不一样可以多试几个源。加速器只对Docker Hub官方镜像仓库生效如果后面用的镜像来自别的私有仓库不会走这个配置。注意不要在公开文章里贴所谓的“稳定加速地址”加速器服务随时可能调整来源也不太可靠。更稳妥的做法是企业内部架设Docker Registry把镜像拉进内网再分发。6.5 端口占用冲突的排查路径宿主机上如果已经有一个PostgreSQL监听5432端口docker-compose里再映射5432就会报Bind for 0.0.0.0:5432 failed: port is already allocated。我遇到过这种问题当时用ss -tlnp | grep 5432查到了已有进程确认是系统自带的旧数据库。处理方式很简单宿主机端口换掉ports: - 5433:5432但要注意改端口之后所有客户端连接串都要同步改包括应用配置、备份脚本、监控探针。我当时忘了改监控探针告警平台连着报了好几天PostgreSQL不可达排查了半天才发现是探针还指着5432端口。7. 补充点进阶玩法与PostGIS地图服务的联动配置最后分享一个我最近在折腾的方向。PostGIS装好只是第一步真正要用起来还得接地图服务。比较常见的组合是PostGIS 地图瓦片服务。瓦片服务可以从PostGIS读取空间数据然后动态生成矢量瓦片供前端渲染。用docker-compose可以再加一个服务tileserver: image: maptiler/tileserver-gl-light ports: - 8080:8080 depends_on: db: condition: service_healthy volumes: - ./tiles:/data这里用了depends_on加condition: service_healthy确保瓦片服务只在PostGIS健康检查通过后才启动。这个语法需要docker-compose 1.29以上版本或者docker compose v2插件支持。当数据库准备好之后在瓦片服务配置里指向PostGIS数据源data: postgis: dbname: gis_db user: gis_user password: ${DB_PASSWORD} host: db port: 5432这样前端地图请求过来时瓦片服务直接实时从PostGIS查空间数据渲染成矢量瓦片省去了预先切片和发布流程。实测在几十万点数据的表上动态生成瓦片的响应时间在200毫秒以内完全够用。如果数据量上到了千万级就得用静态切片方案或者预生成MBTiles文件这个是后话了。我把这套配置写成了模板放团队内部Git仓库新环境部署时只需要改.env里的账号密码直接docker compose up -d就能拉起整套空间数据库和地图服务。比起手动安装PostGIS和地图渲染引擎整个交付时间从大半天压缩到几分钟而且跨环境一致性也解决了。用docker-compose装PostGIS这件事本身不复杂真正决定坑深坑浅的是你选择把哪些细节放在心上。镜像版本锁清楚、数据卷挂明白、初始化脚本写规范、参数调到位这套东西就能一直稳定跑下去。我在实际项目里反复验证过也踩过上面说的丢数据、时间错乱、起不来的坑一次一次把配置模板打磨成现在这个版本。如果你正在规划环境准备工作建议直接复制这份编排文件做底子再按自己的业务场景调整参数能省掉不少起步时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑