博客

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_fused
  • ndvi_2015
  • ndvi_2016
  • …
  • ndvi_2025

安装 MapServer 与依赖#

Homebrew 安装#

bash
brew update brew install mapserver fcgiwrap spawn-fcgi cmake pkgconf apr apr-util curl jpeg-turbo libpng sqlite pixman pcre2 gdal geos

验证#

bash
which mapserv which fcgiwrap which spawn-fcgi mapserv -v

编译安装 MapCache#

下载源码#

bash
cd ~ 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

配置编译环境#

编译安装#

bash
cmake -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

验证#

bash
ls -l /opt/homebrew/bin/mapcache.fcgi ls -l /opt/homebrew/bin/mapcache_seed

创建目录#

bash
mkdir -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

conf
CONFIG 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

nginx
server { 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; } } }

检查并重载:

bash
nginx -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

赋权#

bash
chmod +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#

bash
curl "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#

bash
curl "http://127.0.0.1:8091/mapcache/wmts/1.0.0/WMTSCapabilities.xml"

测试 relief_fused 瓦片#

bash
curl -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 瓦片#

bash
curl -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 瓦片#

bash
curl -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 栅格图层#

mapfile
MAP 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 使用:

nginx
location ~ ^/mapcache(?<path_info>/.*|$)

invalid TILEMATRIXSET WGS84#

原因: 请求值与 grid 名不一致。

解决: 如果 grid 名是 wgs84,请求中也必须使用 wgs84(大小写敏感)。


failed to create lockfile#

原因: 锁目录不存在。

解决:

bash
mkdir -p /tmp/mapcache_locks chmod 777 /tmp/mapcache_locks

WMTS 白底,WMS 透明#

原因通常有两个:

  1. 请求的 tile 本身在数据范围外
  2. 旧 blank tile 已被缓存

解决:

bash
rm -rf /Volumes/MyPassport/xlgl/mapfile/cache/relief_fused /opt/homebrew/etc/gis/scripts/restart-gis.sh

并确保上游 source 中设置了:

xml
TRUE

2015 和 2025 返回同一张瓦片#

原因: 把 MapCache 维度命名成 LAYERS,与 WMS 固定参数冲突。

解决: 不使用动态 LAYERS 维度,改为一个年份一个 tileset。


502 Bad Gateway#

原因: nginx 没拿到有效 FastCGI 响应。

排查:

bash
tail -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:

text
http://127.0.0.1:8091/mapcache/wmts/1.0.0/ndvi_2015/default/wgs84/{z}/{row}/{col}.png

这是目前验证最稳定的方案。每个年份独立 tileset,独立缓存目录,互不干扰,切换年份时只需前端切换 tileset 名称即可。