Apple Silicon Mac 完整部署 MapServer + MapCache:fcgiwrap + spawn-fcgi + Nginx 方案
在 Apple Silicon Mac 上,通过 Homebrew 安装 MapServer、源码编译 MapCache,结合 fcgiwrap + spawn-fcgi + Nginx,构建稳定的 GIS 瓦片服务。覆盖完整安装、配置、启动、重启与排障流程
目标架构#
本方案的最终服务链路如下:
text前端(localhost:5173) ↓ nginx(127.0.0.1:8091) ├── /mapserv → fcgiwrap → /opt/homebrew/bin/mapserv └── /mapcache → spawn-fcgi → mapcache.fcgi (127.0.0.1:19090) ↓ 回源到 MapServer WMS
| 组件 | 说明 |
|---|---|
| 宿主机 | Apple Silicon Mac |
| Web 服务 | nginx |
| MapServer | Homebrew 安装 |
| MapCache | 源码编译 |
| MapServer 通道 | fcgiwrap + Unix socket |
| MapCache 通道 | spawn-fcgi + TCP 19090 |
| 前端访问 | http://127.0.0.1:8091 |
| 投影网格 | WGS84 / EPSG:4326 |
安装目标#
MapServer#
负责发布 WMS,直接读取专题 mapfile。示例:
text/Volumes/MyPassport/xlgl/mapfile/ndvi/ndvi_v4.map /Volumes/MyPassport/xlgl/mapfile/dem/xilingol_relief.map
MapCache#
负责把 MapServer 的 WMS 结果缓存成 WMTS。示例 tileset:
relief_fusedndvi_2015ndvi_2016- …
ndvi_2025
安装 MapServer 与依赖#
Homebrew 安装#
bashbrew update brew install mapserver fcgiwrap spawn-fcgi cmake pkgconf apr apr-util curl jpeg-turbo libpng sqlite pixman pcre2 gdal geos
验证#
bashwhich mapserv which fcgiwrap which spawn-fcgi mapserv -v
编译安装 MapCache#
下载源码#
bashcd ~ curl -L -o mapcache-1.14.1.tar.gz https://download.osgeo.org/mapserver/mapcache-1.14.1.tar.gz tar xf mapcache-1.14.1.tar.gz cd mapcache-1.14.1
配置编译环境#
编译安装#
bashcmake -S . -B build \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX="$BREW_PREFIX" \ -DCMAKE_PREFIX_PATH="$BREW_PREFIX" \ -DWITH_APACHE=0 \ -DWITH_FCGI=1 \ -DWITH_SQLITE=1 \ -DWITH_GDAL=1 \ -DWITH_PCRE2=1 \ -DWITH_MAPSERVER=0 cmake --build build -j"$(sysctl -n hw.ncpu)" cmake --install build
验证#
bashls -l /opt/homebrew/bin/mapcache.fcgi ls -l /opt/homebrew/bin/mapcache_seed
创建目录#
bashmkdir -p /opt/homebrew/etc/mapserver/scripts mkdir -p /opt/homebrew/etc/mapcache/scripts mkdir -p /opt/homebrew/etc/gis/scripts mkdir -p /opt/homebrew/var/mapserver/logs mkdir -p /opt/homebrew/var/mapcache/logs mkdir -p /opt/homebrew/var/run/fcgiwrap mkdir -p /opt/homebrew/var/run/mapcache mkdir -p /Volumes/MyPassport/xlgl/mapfile/cache mkdir -p /tmp/mapcache_locks chmod 777 /tmp/mapcache_locks
MapServer 配置#
mapserver.conf#
文件路径: /opt/homebrew/etc/mapserver/mapserver.conf
confCONFIG ENV MS_ERRORFILE "/opt/homebrew/var/mapserver/logs/mapserver.log" MS_TEMPPATH "/opt/homebrew/var/mapserver/tmp" # 允许 map= 传真实路径,但限制到固定根目录 MS_MAP_PATTERN "^/Volumes/MyPassport/xlgl/mapfile/[A-Za-z0-9_./-]+\\.map$" MS_MAP_BAD_PATTERN "[/\\\\]{2}|[/\\\\]?\\.+[/\\\\]|," END END
不固定 MS_MAPFILE,允许每次通过 map= 参数传入 mapfile,但只允许访问 /Volumes/MyPassport/xlgl/mapfile/ 下的 .map 文件。
fcgiwrap 启动脚本#
文件路径: /opt/homebrew/etc/mapserver/scripts/run-fcgiwrap.sh
bash#!/bin/zsh set -euo pipefail SOCK="/opt/homebrew/var/run/fcgiwrap/mapserver.sock" mkdir -p "${SOCK%/*}" rm -f "$SOCK" exec /opt/homebrew/bin/fcgiwrap -f -c 4 -s "unix:$SOCK" bash chmod +x /opt/homebrew/etc/mapserver/scripts/run-fcgiwrap.sh
MapCache 配置#
配置原则#
本方案的专题结构:
- 一个专题对应一个 mapfile
- 多年份专题在 mapfile 里是多个独立
LAYER
以 NDVI 为例,mapfile 中包含:ndvi_2015、ndvi_2016、…、ndvi_2025
推荐方式:一个年份对应一个 WMTS tileset。
**不要使用 dimension name="LAYERS" 来切年份**,这会与 WMS 的 LAYERS 参数冲突,导致不同年份返回同一张瓦片。
完整 mapcache.xml#
文件路径: /opt/homebrew/etc/mapcache/mapcache.xml
xml<?xml version="1.0" encoding="UTF-8"?> <mapcache> <cache name="disk" type="disk"> <base>/Volumes/MyPassport/xlgl/mapfile/cache</base> <symlink_blank>true</symlink_blank> </cache> <format name="PNG" type="PNG"> <compression>fast</compression> </format> <format name="PNG_FAST" type="PNG"> <compression>fast</compression> </format> <grid name="wgs84"> <srs>EPSG:4326</srs> <unit>degrees</unit> <size>256 256</size> <extent>-180 -90 180 90</extent> <resolutions> 0.703125 0.3515625 0.17578125 0.087890625 0.0439453125 0.02197265625 0.010986328125 0.0054931640625 0.00274658203125 0.001373291015625 0.0006866455078125 0.00034332275390625 0.000171661376953125 0.0000858306884765625 </resolutions> </grid> <!-- 地形融合底图 --> <source name="relief_fused_src" type="wms"> <http> <url>http://127.0.0.1:8091/mapserv</url> </http> <getmap> <params> image/png relief_fused TRUE 1.1.1 /Volumes/MyPassport/xlgl/mapfile/dem/xilingol_relief.map </params> </getmap> <metatile>4 4</metatile> <metabuffer>40</metabuffer> </source> <tileset name="relief_fused"> <source>relief_fused_src</source> <cache>disk</cache> <grid minzoom="0" maxzoom="13">wgs84</grid> <format>PNG_FAST</format> <metadata> <title>地形融合底图</title> <abstract>Xilingol League terrain fusion basemap</abstract> </metadata> <expires>7776000</expires> </tileset> <!-- NDVI:每个年份一个 source + 一个 tileset --> <source name="ndvi_2015_src" type="wms"> <http> <url>http://127.0.0.1:8091/mapserv</url> <connection_timeout>30</connection_timeout> <timeout>60</timeout> </http> <getmap> <params> image/png ndvi_2015 1.1.1 TRUE /Volumes/MyPassport/xlgl/mapfile/ndvi/ndvi_v4.map </params> </getmap> <metatile>4 4</metatile> <metabuffer>20</metabuffer> </source> <tileset name="ndvi_2015"> <source>eqdi_2015_src</source> <cache>disk</cache> <grid minzoom="0" maxzoom="13">wgs84</grid> <format>PNG</format> <metadata> <title>NDVI 2015</title> <abstract>NDVI 2015</abstract> </metadata> <expires>2592000</expires> <auto_expire>86400</auto_expire> </tileset> <source name="ndvi_2025_src" type="wms"> <http> <url>http://127.0.0.1:8091/mapserv</url> <connection_timeout>30</connection_timeout> <timeout>60</timeout> </http> <getmap> <params> image/png xm_eqdi_2025 1.1.1 TRUE /Volumes/MyPassport/xlgl/mapfile/ndvi/ndvi_v4.map </params> </getmap> <metatile>4 4</metatile> <metabuffer>20</metabuffer> </source> <tileset name="ndvi_2025"> <source>ndvi_2025_src</source> <cache>disk</cache> <grid minzoom="0" maxzoom="13">wgs84</grid> <format>PNG</format> <metadata> <title>ndvi 2025</title> <abstract>ndvi 2025</abstract> </metadata> <expires>2592000</expires> <auto_expire>86400</auto_expire> </tileset> <service type="wmts" enabled="true"/> <service type="wms" enabled="true"> <full_wms>assemble</full_wms> <resample_algorithm>bilinear</resample_algorithm> </service> <service type="tms" enabled="true"/> <errors>report</errors> <log_level>info</log_level> <lock_dir>/tmp/mapcache_locks</lock_dir> </mapcache>
ndvi_2015 和 ndvi_2025 仅为示例,2016–2024 按同样模式复制即可。一个年份一个 tileset 是最稳定的方案。
MapCache 启动脚本#
文件路径: /opt/homebrew/etc/mapcache/scripts/start-mapcache.sh
bash#!/bin/zsh set -euo pipefail mkdir -p /opt/homebrew/var/run/mapcache mkdir -p /tmp/mapcache_locks exec /opt/homebrew/bin/spawn-fcgi \ -a 127.0.0.1 \ -p 19090 \ -P /opt/homebrew/var/run/mapcache/mapcache.pid \ -- /opt/homebrew/bin/mapcache.fcgi bash chmod +x /opt/homebrew/etc/mapcache/scripts/start-mapcache.sh
nginx 配置#
文件路径: /opt/homebrew/etc/nginx/servers/mapserver-mapcache.conf
nginxserver { listen 8091; server_name localhost 127.0.0.1; location = /mapserv { include fastcgi_params; fastcgi_pass unix:/opt/homebrew/var/run/fcgiwrap/mapserver.sock; fastcgi_param SCRIPT_FILENAME /opt/homebrew/bin/mapserv; fastcgi_param MAPSERVER_CONFIG_FILE /opt/homebrew/etc/mapserver/mapserver.conf; add_header Access-Control-Allow-Origin "*" always; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; add_header Access-Control-Allow-Headers "*" always; if ($request_method = OPTIONS) { return 204; } } location ~ ^/mapcache(?<path_info>/.*|$) { include fastcgi_params; fastcgi_pass 127.0.0.1:19090; fastcgi_param SCRIPT_FILENAME /opt/homebrew/bin/mapcache.fcgi; fastcgi_param SCRIPT_NAME /mapcache; fastcgi_param PATH_INFO $path_info; fastcgi_param QUERY_STRING $query_string; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param CONTENT_TYPE $content_type; fastcgi_param CONTENT_LENGTH $content_length; add_header Access-Control-Allow-Origin "*" always; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; add_header Access-Control-Allow-Headers "*" always; if ($request_method = OPTIONS) { return 204; } } }
检查并重载:
bashnginx -t nginx -s reload
启动与停止脚本#
stop-gis.sh#
文件路径: /opt/homebrew/etc/gis/scripts/stop-gis.sh
bash#!/bin/zsh set -euo pipefail pkill -f '/opt/homebrew/bin/mapcache.fcgi' 2>/dev/null || true pkill -f '/opt/homebrew/bin/spawn-fcgi.*19090' 2>/dev/null || true rm -f /opt/homebrew/var/run/mapcache/mapcache.pid 2>/dev/null || true pkill -f '/opt/homebrew/bin/fcgiwrap' 2>/dev/null || true rm -f /opt/homebrew/var/run/fcgiwrap/mapserver.sock 2>/dev/null || true sleep 1 echo "GIS services stopped."
start-gis.sh#
文件路径: /opt/homebrew/etc/gis/scripts/start-gis.sh
bash#!/bin/zsh set -euo pipefail mkdir -p /opt/homebrew/var/run/fcgiwrap mkdir -p /opt/homebrew/var/run/mapcache mkdir -p /opt/homebrew/var/mapserver/logs mkdir -p /opt/homebrew/var/mapcache/logs mkdir -p /tmp/mapcache_locks nohup /opt/homebrew/etc/mapserver/scripts/run-fcgiwrap.sh \ > /opt/homebrew/var/mapserver/logs/fcgiwrap.manual.log 2>&1 & sleep 2 /opt/homebrew/etc/mapcache/scripts/start-mapcache.sh sleep 2 echo "GIS services started."
restart-gis.sh#
文件路径: /opt/homebrew/etc/gis/scripts/restart-gis.sh
bash#!/bin/zsh set -euo pipefail /opt/homebrew/etc/gis/scripts/stop-gis.sh sleep 2 /opt/homebrew/etc/gis/scripts/start-gis.sh
status-gis.sh#
文件路径: /opt/homebrew/etc/gis/scripts/status-gis.sh
bash#!/bin/zsh set -euo pipefail echo "== fcgiwrap ==" ps aux | egrep '[f]cgiwrap|[m]apserv' || true ls -l /opt/homebrew/var/run/fcgiwrap/mapserver.sock || true echo echo "== mapcache ==" ps aux | egrep '[m]apcache\.fcgi|[s]pawn-fcgi' || true lsof -nP -iTCP:19090 -sTCP:LISTEN || true echo echo "== nginx ==" lsof -nP -iTCP:8091 -sTCP:LISTEN || true
赋权#
bashchmod +x /opt/homebrew/etc/gis/scripts/stop-gis.sh chmod +x /opt/homebrew/etc/gis/scripts/start-gis.sh chmod +x /opt/homebrew/etc/gis/scripts/restart-gis.sh chmod +x /opt/homebrew/etc/gis/scripts/status-gis.sh
常用命令速查#
bash /opt/homebrew/etc/gis/scripts/start-gis.sh
bash /opt/homebrew/etc/gis/scripts/stop-gis.sh
bash /opt/homebrew/etc/gis/scripts/restart-gis.sh
bash /opt/homebrew/etc/gis/scripts/status-gis.sh
测试方式#
测试 MapServer WMS#
bashcurl "http://127.0.0.1:8091/mapserv?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetCapabilities&map=/Volumes/MyPassport/xlgl/mapfile/ndvi/ndvi_v4.map"
测试 MapCache WMTS Capabilities#
bashcurl "http://127.0.0.1:8091/mapcache/wmts/1.0.0/WMTSCapabilities.xml"
测试 relief_fused 瓦片#
bashcurl -o /tmp/relief.png \ "http://127.0.0.1:8091/mapcache/wmts/1.0.0/relief_fused/default/wgs84/5/8/52.png" open /tmp/relief.png
测试 NDVI 2015 瓦片#
bashcurl -o /tmp/ndvi2015.png \ "http://127.0.0.1:8091/mapcache/wmts/1.0.0/ndvi_2015/default/wgs84/5/8/52.png" open /tmp/ndvi2015.png
测试 NDVI 2025 瓦片#
bashcurl -o /tmp/ndvi2025.png \ "http://127.0.0.1:8091/mapcache/wmts/1.0.0/ndvi_2025/default/wgs84/5/8/52.png" open /tmp/ndvi2025.png
Mapfile 编写规范#
Relief 栅格图层#
mapfileMAP NAME "xilingol_relief" STATUS ON SIZE 800 600 EXTENT 111.0 41.5 120.5 46.8 UNITS DD SHAPEPATH "../dem/" IMAGETYPE "png" PROJECTION "init=epsg:4326" END OUTPUTFORMAT NAME "png" DRIVER "AGG/PNG" MIMETYPE "image/png" IMAGEMODE RGBA EXTENSION "png" TRANSPARENT ON END WEB METADATA "wms_title" "Xilingol DEM Shaded Relief" "wms_srs" "EPSG:4326 EPSG:3857" "ows_enable_request" "*" END END LAYER NAME "relief_fused" TYPE RASTER STATUS ON DATA "./relief_fused_final_120m_clip.tif" PROJECTION "init=epsg:4326" END PROCESSING "BANDS=1,2,3" OFFSITE 255 255 255 METADATA "wms_title" "地形晕渲图" "wms_srs" "EPSG:4326 EPSG:3857" END END END
NDVI 栅格图层编写要点#
IMAGETYPE "png"OUTPUTFORMAT NAME "png"- 每个年份一个
LAYER - 图层名称必须与 MapCache tileset source 中的
LAYERS=完全一致
常见问题与排查#
CORS 错误#
原因: 前端 5173 与服务 8091 跨域。
解决: nginx 在 /mapserv 和 /mapcache 上加 CORS header(已在上方配置中包含)。
unknown service wmts#
原因: WMTS 请求路径错误或 PATH_INFO 配置错误。
解决: 使用正确路径格式:
text/mapcache/wmts/1.0.0/WMTSCapabilities.xml
并确保 nginx 使用:
nginxlocation ~ ^/mapcache(?<path_info>/.*|$)
invalid TILEMATRIXSET WGS84#
原因: 请求值与 grid 名不一致。
解决: 如果 grid 名是 wgs84,请求中也必须使用 wgs84(大小写敏感)。
failed to create lockfile#
原因: 锁目录不存在。
解决:
bashmkdir -p /tmp/mapcache_locks chmod 777 /tmp/mapcache_locks
WMTS 白底,WMS 透明#
原因通常有两个:
- 请求的 tile 本身在数据范围外
- 旧 blank tile 已被缓存
解决:
bashrm -rf /Volumes/MyPassport/xlgl/mapfile/cache/relief_fused /opt/homebrew/etc/gis/scripts/restart-gis.sh
并确保上游 source 中设置了:
xmlTRUE
2015 和 2025 返回同一张瓦片#
原因: 把 MapCache 维度命名成 LAYERS,与 WMS 固定参数冲突。
解决: 不使用动态 LAYERS 维度,改为一个年份一个 tileset。
502 Bad Gateway#
原因: nginx 没拿到有效 FastCGI 响应。
排查:
bashtail -n 50 /opt/homebrew/var/log/nginx/error.log tail -n 100 /opt/homebrew/var/mapcache/logs/mapcache.stderr.log lsof -nP -iTCP:19090 -sTCP:LISTEN
推荐的最终使用方式#
对于多年份专题,前端直接按 tileset 名访问,不做动态 layer 拼接:
| tileset 名 | WMTS 访问路径模板 |
|---|---|
ndvi_2015 |
/mapcache/wmts/1.0.0/ndvi_2015/default/wgs84/{z}/{row}/{col}.png |
ndvi_2016 |
/mapcache/wmts/1.0.0/ndvi_2016/default/wgs84/{z}/{row}/{col}.png |
| … | … |
ndvi_2025 |
/mapcache/wmts/1.0.0/ndvi_2025/default/wgs84/{z}/{row}/{col}.png |
示例完整 URL:
texthttp://127.0.0.1:8091/mapcache/wmts/1.0.0/ndvi_2015/default/wgs84/{z}/{row}/{col}.png
这是目前验证最稳定的方案。每个年份独立 tileset,独立缓存目录,互不干扰,切换年份时只需前端切换 tileset 名称即可。