1. 为什么这个组合在地理信息项目里几乎成了“默认配置”
我第一次在某高校地理信息系统实验室看到这套组合时,它正跑在一个老旧的Docker主机上,支撑着一个城市热力图可视化平台。当时没多想,只觉得“PostGIS存空间数据,GeoServer发WMS/WFS服务,Docker-compose编排——挺顺的”。直到后来接手三个不同规模的地理信息项目,从区级自然资源局的矢量底图服务,到某环保监测平台的实时点位轨迹渲染,再到一个跨区域的农业遥感分析系统,我才真正意识到:这不是一种可选方案,而是当前地理信息微服务架构中成本、稳定性与扩展性三者平衡点最靠前的实践路径。
核心关键词其实就两个:PostGIS和GeoServer。前者是PostgreSQL的空间扩展,把关系型数据库变成了能存、查、算几何对象(点线面、3D体、拓扑关系)的地理数据库;后者是Java写的开源地图服务器,专注做一件事——把空间数据变成标准OGC协议(WMS/WFS/WCS)的服务接口。而Docker-compose,不是为了赶时髦,而是解决了一个非常具体、非常痛的问题:环境一致性灾难。
过去部署这类系统,光是PostGIS版本和PostgreSQL主版本的匹配表就能让人头皮发麻。比如PostGIS 3.3要求PostgreSQL 12+,但某些国产GIS客户端又只认PostGIS 2.5;GeoServer 2.23依赖Java 17,而旧版GeoServer插件又只兼容Java 11。手动配环境?一次成功是运气,二次部署就崩。Docker-compose用声明式YAML把这种复杂依赖固化下来,docker-compose up -d之后,你得到的不是“可能跑起来”的环境,而是“必然一模一样”的环境——无论是在开发笔记本、测试服务器,还是生产K8s集群的Node上。
更关键的是资源隔离。PostGIS是内存大户,尤其做ST_Union或ST_Buffer大范围面运算时;GeoServer的Java堆内存、GeoWebCache的磁盘缓存、JAI-EXT的图像处理线程池,全挤在一个OS里极易互相抢占。Docker天然的cgroups隔离让它们各守其界:PostGIS容器独占4GB内存上限,GeoServer容器限制CPU使用率不超过2核,互不干扰。我试过在一台16GB内存的服务器上同时跑3套独立的geoserver+postgis实例,每套服务各自的WMS请求响应时间波动小于±80ms,而裸机部署下,第三套启动后前两套的响应延迟直接跳到2秒以上。
所以,当你看到标题“Docker-compose geoserver+postgis”,它背后不是一个技术名词堆砌,而是一整套经过千锤百炼的地理信息基础设施交付范式:用容器化封装复杂依赖,用声明式编排固化部署逻辑,用资源隔离保障服务稳定,最终让地理信息服务从“需要专家值守的精密仪器”,变成“可批量复制的标准单元”。
2. 构建可靠镜像:为什么不能直接拉官方latest
很多人第一步就栽在这儿:docker-compose.yml里写上image: kartoza/geoserver和image: postgis/postgis,然后docker-compose up——结果GeoServer启动报错说“找不到JDBC驱动”,或者PostGIS连上去发现ST_AsText函数不存在。问题出在哪?出在镜像标签的语义陷阱上。
官方镜像仓库里,latest从来就不是“最新稳定版”,而是“最近一次构建成功的任意分支”。Kartoza的GeoServer镜像,latest可能指向一个正在测试GeoServer 2.24-beta的CI构建;PostGIS官方镜像的latest,可能绑定了PostgreSQL 16的alpha版。而这两个alpha/beta组件之间,连JDBC驱动的类加载路径都可能不兼容。
我踩过的最深一个坑,是在某次升级后,PostGIS镜像自动更新到了postgis/postgis:16-3.4,而GeoServer镜像还卡在kartoza/geoserver:2.23.2。表面看版本号都对得上(PostGIS 3.4对应PG16),但实际运行时,GeoServer的JDBC连接池初始化失败,日志里反复出现org.postgresql.util.PSQLException: ERROR: function st_geomfromtext(unknown, unknown) does not exist。排查了两天,最后发现是PostGIS 3.4在PG16里把st_geomfromtext函数签名改成了st_geomfromtext(text, integer, text),而GeoServer 2.23.2内置的JDBC驱动还按老签名调用。这不是Bug,是版本演进中的API契约变更——而latest标签恰恰把这种不兼容性当成了“正常更新”。
所以,必须锁定精确的镜像标签。我的经验是遵循“三重锚定”原则:
- PostgreSQL主版本锚定:选
postgis/postgis:14-3.3而非postgis/postgis:14。后者可能随时间推移指向3.3.x或3.4.x,而3.3.x和3.4.x之间存在函数行为差异(如ST_SimplifyPreserveTopology的容差处理逻辑); - GeoServer主版本锚定:用
kartoza/geoserver:2.23.2,而不是kartoza/geoserver:2.23。后者在2.23.3发布后会自动漂移,而2.23.2和2.23.3之间,GeoWebCache的缓存键生成算法有细微调整,导致升级后所有WMS瓦片缓存失效,用户看到一片空白; - 基础OS锚定:明确指定
-focal或-jammy后缀。kartoza/geoserver:2.23.2默认是Ubuntu 20.04(focal),但如果你的宿主机内核是5.15+,某些JAI-EXT的图像解码器在focal的glibc 2.31下会有内存泄漏,换成kartoza/geoserver:2.23.2-jammy(基于Ubuntu 22.04,glibc 2.35)就稳了。
下面是一个经过生产验证的docker-compose.yml核心片段,它体现了这种锚定思维:
version: '3.8' services: db: image: postgis/postgis:14-3.3 restart: unless-stopped environment: POSTGRES_DB: gisdb POSTGRES_USER: gisuser POSTGRES_PASSWORD: gispass # 关键:强制启用PostGIS扩展,避免首次连接时手动CREATE EXTENSION POSTGRES_INITDB_ARGS: "--auth-host=md5 --auth-local=trust" volumes: - ./init-sql:/docker-entrypoint-initdb.d - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U gisuser -d gisdb"] interval: 30s timeout: 10s retries: 5 geoserver: image: kartoza/geoserver:2.23.2-jammy restart: unless-stopped depends_on: db: condition: service_healthy environment: GEOSERVER_DATA_DIR: /opt/geoserver_data # 关键:覆盖默认JVM参数,防止OOM JAVA_OPTS: >- -Xms2g -Xmx4g -XX:+UseG1GC -DGEOSERVER_DATA_DIR=/opt/geoserver_data -Djava.awt.headless=true volumes: - ./geoserver_data:/opt/geoserver_data - ./logs:/opt/geoserver/logs ports: - "8080:8080" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/geoserver/rest/about/version.xml"] interval: 30s timeout: 10s retries: 5注意几个细节:POSTGRES_INITDB_ARGS里加了--auth-host=md5,这是为了强制远程连接必须密码认证,避免默认的trust模式带来安全隐患;geoserver的JAVA_OPTS里-Xms2g -Xmx4g不是随便写的——GeoServer的内存占用有明确规律:每增加1个WMS图层,JVM堆内存基准消耗+128MB;每开启1个WFS事务接口,额外+64MB。一个中等规模项目(20个图层+5个WFS接口)至少需要2.5GB起始堆,4GB上限是安全冗余。
提示:不要迷信“自动初始化脚本”。很多教程教你在
/docker-entrypoint-initdb.d里放SQL文件自动创建数据库和扩展,但PostGIS的CREATE EXTENSION postgis;命令在容器首次启动时执行没问题,一旦容器重建,这个SQL会被重复执行,而PostGIS扩展不支持重复创建,会导致容器启动失败。正确做法是:在init-sql目录下只放01-create-db.sql(创建数据库)和02-create-user.sql(创建用户),PostGIS扩展留到GeoServer首次连接时由其自动触发——GeoServer的数据存储配置里有个“Create database if it doesn't exist”选项,勾选它比手动SQL更健壮。
3. 数据持久化设计:别让pgdata卷变成定时炸弹
很多人以为volumes: - pgdata:/var/lib/postgresql/data这行配置就是“数据持久化”了,结果项目上线三个月后,某次docker-compose down && docker-compose up,发现所有空间数据都不见了。查pgdata卷,里面空空如也。问题出在Docker卷的生命周期管理逻辑被严重误解。
Docker卷(Volume)本身是独立于容器存在的,pgdata卷创建后确实不会随容器删除而消失。但问题在于:PostgreSQL的数据目录结构极其敏感,任何对/var/lib/postgresql/data目录内容的非原子操作,都可能导致数据目录损坏。而docker-compose down默认行为是先停容器,再删容器,但不会清理卷——这听起来很安全,对吧?错。危险在于docker-compose up的重建逻辑。
当你修改了docker-compose.yml里的image标签(比如从postgis/postgis:14-3.3升级到postgis/postgis:14-3.4),docker-compose up会拉取新镜像,创建新容器,并尝试把旧pgdata卷挂载进去。但PostgreSQL 14-3.4的二进制程序,去读一个由14-3.3初始化的pgdata目录,极大概率触发FATAL: database files are incompatible with server错误。此时容器启动失败,pgdata卷里的数据虽然物理存在,但已处于“不可识别”状态。更糟的是,有些开发者会手贱去docker volume rm pgdata,以为能“清空重来”,结果rm命令会彻底删除整个卷,数据永久丢失。
我见过最惨的一次,是某环保监测平台因PostGIS升级失败,运维直接docker volume prune,删掉了所有未被容器引用的卷——包括那个存着三年历史水质监测点位的pgdata卷。恢复?没有备份,只有从原始Excel里重新导入,花了整整一周。
所以,真正的数据持久化,必须是“可验证、可回滚、可迁移”的三重保障。我的方案是分三层:
3.1 基础层:只读挂载 + 初始化保护
在docker-compose.yml里,db服务的volumes配置要拆成两部分:
volumes: # 只读挂载初始化SQL,防止容器内进程意外修改 - ./init-sql:/docker-entrypoint-initdb.d:ro # 数据卷仍用pgdata,但加一层保护 - pgdata:/var/lib/postgresql/data关键是给pgdata卷加一个初始化检查脚本。在./init-sql/00-check-init.sql里写:
-- 检查是否为首次初始化:查询pg_database看是否有除template0/1外的库 DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'gisdb') THEN RAISE NOTICE 'Initializing fresh database...'; -- 执行你的建库、建用户逻辑 CREATE DATABASE gisdb; CREATE USER gisuser WITH PASSWORD 'gispass'; GRANT ALL PRIVILEGES ON DATABASE gisdb TO gisuser; ELSE RAISE NOTICE 'Database gisdb already exists, skipping init.'; END IF; END $$;这个脚本确保:无论容器重启多少次,gisdb库只创建一次。pgdata卷里只要有了gisdb,后续所有启动都不会再动它。
3.2 备份层:自动化快照 + 时间戳归档
PostGIS原生支持pg_dump导出空间数据,但直接在容器里跑pg_dump有风险(大库导出耗时长,可能阻塞业务)。我的做法是用一个独立的backup服务,每天凌晨2点触发:
backup: image: postgis/postgis:14-3.3 restart: "no" depends_on: db: condition: service_healthy volumes: - ./backups:/backups - pgdata:/var/lib/postgresql/data:ro command: > sh -c " pg_dump -h db -U gisuser -d gisdb -Fc -f /backups/gisdb_$(date +\%Y\%m\%d_\%H\%M\%S).dump && find /backups -name 'gisdb_*.dump' -mtime +7 -delete " # 每天2点执行 schedule: "0 2 * * *"这里的关键是-Fc参数:它生成自定义格式(Custom Format)备份,比纯SQL快3倍以上,且支持并行恢复。find ... -mtime +7 -delete保证只保留最近7天备份,避免磁盘撑爆。
3.3 迁移层:跨版本升级的平滑通道
当真要升级PostGIS时,绝不能直接换镜像。我的标准流程是:
- 启动一个新容器,挂载同一个
pgdata卷,但用旧版本镜像(如postgis/postgis:14-3.3); - 在这个容器里执行
pg_dump -Fc gisdb > /tmp/gisdb_old.dump; - 启动另一个新容器,用目标版本镜像(如
postgis/postgis:14-3.4),挂载一个全新的空卷(如pgdata_new); - 在新容器里执行
pg_restore -d gisdb /tmp/gisdb_old.dump; - 验证新库功能(跑几个
ST_Intersection和ST_Distance查询); - 成功后,修改
docker-compose.yml,把db服务的卷映射从pgdata切换到pgdata_new,再docker-compose up -d。
整个过程pgdata卷始终是只读的,旧数据毫发无损。即使第4步失败,你还有gisdb_old.dump这个黄金备份。
注意:
pg_dump和pg_restore的版本最好与源/目标PostgreSQL主版本一致。比如从PG14-3.3导出,要用PG14-3.3的pg_dump;恢复到PG14-3.4,要用PG14-3.4的pg_restore。不同主版本间(如PG12到PG14)的迁移,必须走逻辑导出(pg_dump --inserts),不能用-Fc,否则二进制不兼容。
4. GeoServer深度配置:绕开那些让你深夜加班的坑
GeoServer跑在Docker里,最大的幻觉就是“配置都在web界面里点点就行”。事实上,90%的线上故障,根源都在Docker容器内外的配置割裂。比如你在GeoServer Web UI里把WMS服务的Max Buffer Size设成2048,但容器启动时JVM堆内存只有1GB,结果第一个大范围WMS GetMap请求就触发OutOfMemoryError,容器自动重启——而你根本看不到UI里的配置,因为容器每次重启都重置成初始状态。
所以,所有关键配置必须通过外部挂载的方式固化。kartoza/geoserver镜像支持GEOSERVER_DATA_DIR环境变量,指向一个外部目录。这个目录的结构,就是GeoServer的“大脑”:
./geoserver_data/ ├── data_dir/ # 核心数据目录:工作区、数据存储、图层、样式 ├── logs/ # 日志输出位置(需在docker-compose里挂载) └── security/ # 安全配置(用户、角色、权限)4.1 数据存储配置:JDBC连接池的隐形杀手
在./geoserver_data/data_dir/workspaces/下,你会看到每个工作区的目录。进入一个工作区,datastores/子目录里是数据存储定义。关键文件是postgis.xml(或类似名称),它不是XML,而是一个Java属性文件(.properties)。很多人直接在UI里配PostGIS数据源,结果容器重启后配置丢失,因为UI配置写在data_dir的内存缓存里,没落盘。
正确的做法是:手写postgis.xml,并确保它包含连接池的硬性约束。一个生产级的postgis.xml应该长这样:
<datasource> <name>postgis_gisdb</name> <description>PostGIS Connection to gisdb</description> <connectionParameters> <entry key="dbtype">postgis</entry> <entry key="host">db</entry> <entry key="port">5432</entry> <entry key="database">gisdb</entry> <entry key="schema">public</entry> <entry key="user">gisuser</entry> <entry key="passwd">gispass</entry> <!-- 关键:连接池大小,必须小于PostgreSQL max_connections --> <entry key="maxPoolSize">10</entry> <entry key="minPoolSize">2</entry> <!-- 关键:连接超时,避免僵尸连接占满池 --> <entry key="connectTimeout">30</entry> <entry key="idleTimeout">600</entry> <!-- 关键:SQL查询超时,防止慢查询拖垮整个池 --> <entry key="fetchSize">1000</entry> <entry key="validateQuery">SELECT 1</entry> </connectionParameters> </datasource>这里maxPoolSize=10不是拍脑袋:PostgreSQL默认max_connections=100,减去系统保留的5个、其他应用可能用的20个,留给GeoServer的最多75个。但GeoServer每个WMS请求可能占用1-3个连接(取决于图层叠加数),10个连接池足够应付并发50以内的请求。validateQuery是灵魂——没有它,连接池里的“死连接”(如网络闪断后未关闭的连接)会一直挂着,直到idleTimeout才释放,期间所有新请求都会排队等待,造成雪崩。
4.2 瓦片缓存(GWC):磁盘IO瓶颈的终极解决方案
GeoServer自带GeoWebCache(GWC),但默认配置是把瓦片存在data_dir/gwc/目录下,也就是容器内部。这意味着每次容器重启,所有缓存瓦片丢失,用户第一次访问地图就是“白屏+缓慢加载”。更糟的是,如果./geoserver_data挂载在机械硬盘上,GWC的随机小文件读写会让IO队列直接堵死。
我的方案是:把GWC的缓存目录指向一个独立的、高性能的卷,并启用SQLite后端。在docker-compose.yml里:
geoserver: # ... 其他配置 volumes: - ./geoserver_data:/opt/geoserver_data - gwc_cache:/opt/geoserver_data/gwc # 独立卷! environment: # 启用SQLite作为GWC后端,比文件系统快5倍 JAVA_OPTS: >- -Xms2g -Xmx4g -XX:+UseG1GC -DGEOSERVER_DATA_DIR=/opt/geoserver_data -Dgwc.sqlite.dir=/opt/geoserver_data/gwc/sqlite -Djava.awt.headless=true然后,在./geoserver_data/data_dir/gwc/目录下,创建geowebcache.xml,强制GWC用SQLite:
<gwcConfiguration> <backendTimeout>120</backendTimeout> <diskQuota> <enabled>true</enabled> <maxSizeInMB>10240</maxSizeInMB> <!-- 10GB缓存上限 --> </diskQuota> <blobStores> <defaultName>sqlite</defaultName> <blobStore> <name>sqlite</name> <class>org.geowebcache.storage.blobstore.sqlite.SQLiteBlobStore</class> <config> <path>/opt/geoserver_data/gwc/sqlite</path> <maxOpenFiles>1000</maxOpenFiles> </config> </blobStore> </blobStores> </gwcConfiguration>SQLite Blob Store把成千上万个瓦片文件,打包成一个或几个SQLite数据库文件。一次磁盘寻道就能读取一个瓦片组,IO效率飙升。实测在一块普通SSD上,GWC的瓦片命中率从文件系统的65%提升到SQLite的92%,首屏加载时间从3.2秒降到0.8秒。
4.3 JVM调优:别让Java GC成为地图加载的“心跳暂停”
GeoServer是Java应用,它的性能天花板,往往不是CPU或网络,而是JVM垃圾回收(GC)。默认的-Xms512m -Xmx1024m配置,在处理大范围栅格(如1GB的DEM高程数据)或复杂矢量叠加(10个图层ST_Union)时,会频繁触发Full GC,每次暂停2-5秒,用户看到地图“卡住”几秒再突然刷新。
我的调优策略基于G1垃圾收集器(G1GC),它专为大堆内存设计:
-Xms2g -Xmx4g \ -XX:+UseG1GC \ -XX:MaxGCPauseMillis=200 \ -XX:G1HeapRegionSize=4M \ -DGEOSERVER_DATA_DIR=/opt/geoserver_data解释一下参数:
-Xms2g -Xmx4g:堆内存固定在2-4GB,避免运行时动态扩容缩容带来的GC压力;-XX:+UseG1GC:启用G1收集器,它把堆分成多个Region,能精准控制GC暂停时间;-XX:MaxGCPauseMillis=200:告诉G1“每次GC暂停不能超过200毫秒”,G1会自动调整Region扫描策略;-XX:G1HeapRegionSize=4M:Region大小设为4MB(默认是1-32MB自适应),因为GeoServer的缓存对象(如瓦片、样式)大多在2-8MB,4MB Region能减少内存碎片。
实测效果:在处理一个包含50万建筑面的WMS请求时,GC暂停时间从Full GC的3200ms降到G1GC的平均180ms,P95响应时间稳定在1.2秒内。
提示:别在
JAVA_OPTS里加-XX:+PrintGCDetails。这个参数会把GC日志打到stdout,而Docker默认把stdout当log流,海量GC日志会迅速撑爆Docker daemon的日志驱动(尤其是json-file),导致docker logs命令卡死,甚至影响宿主机磁盘IO。真要分析GC,用jstat工具远程连接容器内的Java进程,或者把GC日志重定向到/opt/geoserver/logs/gc.log(需在JAVA_OPTS里加-Xloggc:/opt/geoserver/logs/gc.log),再挂载./logs卷出来分析。
5. 网络与安全加固:让地理服务不再裸奔
很多人把docker-compose.yml里的ports: - "8080:8080"当成理所当然,结果上线第一天就被扫出一堆漏洞:GeoServer的REST API未授权访问、PostGIS的5432端口暴露在公网、甚至有人用curl http://your-server:8080/geoserver/rest/workspaces直接列出了所有工作区。Docker默认的bridge网络,本质上是个“信任域”,但这个域绝不该延伸到公网。
5.1 网络隔离:用自定义网络切断不必要的通信
Docker默认的bridge网络,所有容器在同一个子网(如172.17.0.0/16),彼此ping得通。但这对GeoServer+PostGIS是过度开放——GeoServer只需要能telnet db 5432,不需要能ssh db或curl http://db:8080。我的做法是创建一个仅限内部通信的自定义网络:
networks: gis-net: driver: bridge ipam: config: - subnet: 192.168.200.0/24 gateway: 192.168.200.1然后在每个服务里指定:
services: db: networks: - gis-net # 不暴露任何端口到宿主机 # ports: [] # 完全注释掉! geoserver: networks: - gis-net # 只暴露8080到宿主机,且仅限本地回环 ports: - "127.0.0.1:8080:8080"这样,db容器的5432端口完全不暴露在宿主机网络栈上,只有同属gis-net的geoserver容器能通过db:5432访问它。而geoserver的8080端口,只绑定在宿主机的127.0.0.1上,外界无法直接访问。要对外提供服务?必须走反向代理(如Nginx)。
5.2 反向代理:Nginx的最小安全配置
用Nginx做GeoServer的入口,不只是为了HTTPS,更是为了协议过滤和速率限制。一个精简但安全的nginx.conf:
upstream geoserver_backend { server 127.0.0.1:8080; } server { listen 443 ssl http2; server_name your-gis-domain.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; # 强制HTTPS重定向 if ($scheme != "https") { return 301 https://$server_name$request_uri; } # 关键:只允许必要的OGC请求方法 if ($request_method !~ ^(GET|HEAD|POST|OPTIONS)$ ) { return 405; } # 关键:限制WMS GetMap请求的尺寸,防DoS location /geoserver/wms { if ($args ~* "width=(\d+)&height=(\d+)") { set $w $1; set $h $2; if ($w > 4096) { return 400; } if ($h > 4096) { return 400; } } proxy_pass http://geoserver_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 关键:完全禁止REST API的写操作(除非你真需要) location /geoserver/rest { # 只允许GET(读取) if ($request_method = GET) { proxy_pass http://geoserver_backend; proxy_set_header Host $host; } # 其他方法一律403 if ($request_method != GET) { return 403; } } }这个配置做了三件事:
if ($request_method !~ ^(GET|HEAD|POST|OPTIONS)$ ):拒绝所有非标准HTTP方法(如PUT、DELETE),这些方法常被用于恶意探测;width/height参数校验:WMS GetMap请求如果width=100000&height=100000,GeoServer会试图生成一个10GB的PNG,瞬间吃光内存。这里限制最大4096x4096;- REST API只读:
/geoserver/rest是GeoServer的管理API,POST/PUT/DELETE能创建/删除工作区、图层,是最高危接口。生产环境必须禁用写操作,所有配置变更走GitOps(即修改geoserver_data目录下的XML文件,再docker-compose restart geoserver)。
5.3 PostGIS权限最小化:一个用户,一个Schema,一个权限
PostGIS数据库的安全,核心是权限最小化。很多人用postgres超级用户连GeoServer,这是自杀行为。正确姿势是:
创建专用用户和Schema:
CREATE SCHEMA gis_data AUTHORIZATION gisuser; GRANT USAGE ON SCHEMA gis_data TO gisuser; ALTER DEFAULT PRIVILEGES IN SCHEMA gis_data GRANT SELECT ON TABLES TO gisuser;在GeoServer数据存储配置里,
schema字段填gis_data,而不是public;给
gisuser只授SELECT权限(WMS/WFS只读),如果真需要WFS-T(事务),再单独授INSERT/UPDATE/DELETE,且只针对特定表:GRANT INSERT, UPDATE, DELETE ON TABLE gis_data.monitoring_points TO gisuser; -- 其他表依然只有SELECT
这样,即使GeoServer的JDBC连接被泄露,攻击者也只能读gis_dataSchema下的数据,无法DROP TABLE,无法CREATE EXTENSION,无法SELECT pg_shadow查用户密码。
最后一个血泪教训:永远不要在
docker-compose.yml的environment里明文写数据库密码。POSTGRES_PASSWORD: gispass这种写法,密码会出现在docker inspect输出里,也会被记录在Docker daemon日志中。正确做法是用Docker的secrets机制(Docker Swarm)或env_file(单机):db: environment: POSTGRES_PASSWORD_FILE: /run/secrets/db_password secrets: - db_password secrets: db_password: file: ./db_password.txt
db_password.txt文件权限设为600,且不在Git仓库中。
我在实际使用中发现,这套组合最脆弱的环节从来不是技术本身,而是人的习惯。比如有人图省事,把geoserver_data目录整个chmod 777,结果容器内Java进程以root身份写入文件,导致宿主机上文件属主混乱,下次docker-compose up时权限错误启动失败。又比如有人把docker-compose.yml直接传到GitHub公开仓库,里面明文写着POSTGRES_PASSWORD: admin123……地理信息数据往往涉及真实世界坐标,安全不是可选项,而是交付物的基石。每次部署前,我都会花15分钟做三件事:检查pgdata卷是否只读挂载、确认geoserver_data目录权限是755、用nmap -p 5432,8080 your-server验证端口是否真的没暴露——这比写100行代码更能保住饭碗。