macOS
What this covers
ZoneMinder builds, installs and runs on macOS from source. There is no package and no Homebrew formula yet, so everything here is manual, and there is no launchd job — you start the daemons yourself or write your own plist.
Two prefixes are in play and it is worth keeping them apart. Homebrew’s prefix
is where the dependencies come from — /opt/homebrew on Apple Silicon,
/usr/local on Intel — and the commands below write it as $(brew --prefix)
so they work on either. ZoneMinder’s own install prefix is separate and defaults
to /usr/local on both, which is what the paths in this guide assume. Pass
-DCMAKE_INSTALL_PREFIX= at configure time to put it somewhere else, and
adjust the paths here to match.
Local USB cameras are not supported. V4L2 is a Linux interface and has no macOS
equivalent in ZoneMinder, so configure reports Could NOT find V4L2 and that
is expected. Network cameras — RTSP, HTTP, ONVIF — work normally, and that is
what ZoneMinder is mostly used with anyway.
Prerequisites
Xcode command line tools and Homebrew:
xcode-select --install
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Dependencies
To build:
brew install \
catch2 ffmpeg gsoap jpeg-turbo mosquitto mysql-client \
nlohmann-json openssl@3 pcre2
cmake and pkg-config come with the command line tools, and curl comes
from the SDK, so none of those need installing.
To run, you also need a database, PHP and a web server. macOS still ships Apache
at /usr/sbin/httpd, but it has not shipped PHP since Monterey, so PHP comes
from Homebrew either way:
brew install mariadb php
Perl modules
Some modules ZoneMinder needs are not present in either Perl, and configure only warns about them rather than failing, so it is easy to end up with a clean build and a system whose Perl daemons do not work.
Sys::MmapbacksZoneMinder::Memory::Mapped. Every Perl daemon reads monitor state through it, so without itzmdc.pl,zmwatch.plandzmpkg.plcannot see your monitors at all.Date::Manipis used byzmfilter.pl,Event.pmandFilter.pm.A database driver. ZoneMinder prefers
DBD::MariaDB, which drives both MariaDB and MySQL.
Note
-DZM_NO_MMAP=ON is not a way around Sys::Mmap. It switches ZoneMinder
to SysV shared memory, and macOS caps that at 4 MiB per segment
(kern.sysv.shmmax) across at most 8 segments. A single 1080p RGB frame is
larger than one segment. Memory mapping is the only workable mode here.
Using a Homebrew Perl
Recommended. Apple has deprecated the Perl it bundles and it will eventually be removed, and this keeps ZoneMinder off the system directories entirely.
brew install perl cpanminus
cpanm --notest Sys::Mmap Date::Manip DBI LWP::UserAgent
A Homebrew Perl has none of the extras Apple bundles, so it needs DBI and
LWP::UserAgent as well.
DBD::MariaDB needs help. Its configure step asks mysql_config for link
flags and gets -lzstd -lssl -lcrypto back, but a Homebrew Perl’s ldflags
carry no -L$(brew --prefix)/lib, so the check fails with Can't
link/include C library 'zstd', 'ssl', 'crypto', aborting. Pass the paths in:
export PATH="$(brew --prefix mysql-client)/bin:$PATH"
cpanm --notest \
--configure-args="--libs=\"-L$(brew --prefix mysql-client)/lib -L$(brew --prefix)/lib -lmysqlclient -lz -lzstd -lssl -lcrypto -lresolv\" --cflags=\"-I$(brew --prefix mysql-client)/include/mysql\"" \
DBD::MariaDB
Then configure ZoneMinder against that Perl:
-DPERL_EXECUTABLE=$(brew --prefix)/bin/perl
ZoneMinder’s own modules install to that Perl’s vendorlib, under
$(brew --prefix)/lib/perl5/vendor_perl/<version>, which sits outside the
Cellar and survives Perl upgrades. The CPAN modules above do not: cpanm
writes them to sitelib, which resolves into the versioned Cellar directory
and is erased whenever Homebrew upgrades Perl. Reinstall them afterwards, or set
up local::lib as brew info perl describes.
Using the system Perl
Fewer modules are needed, because Apple bundles DBI and LWP::UserAgent:
sudo cpan Sys::Mmap Date::Manip
You still need a database driver, and it needs the same link flags as above.
Modules install to /Library/Perl/<version>, which needs sudo.
Build
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_TEST_SUITE=ON \
-DCMAKE_PREFIX_PATH="$(brew --prefix mysql-client);$(brew --prefix)" \
-DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)" \
-DCMAKE_C_FLAGS="-I$(brew --prefix mysql-client)/include" \
-DCMAKE_CXX_FLAGS="-I$(brew --prefix mysql-client)/include"
cmake --build build -j"$(sysctl -n hw.ncpu)"
The mysql-client flags are not optional. Homebrew keeps that formula
keg-only, so it is not on the default include path and the bare find_library
call in CMakeLists.txt will not find its headers. Leave them out and the
build stops at 'mysql/mysql.h' file not found.
To check the build, from the tests directory — the font tests load fixtures
by relative path, so the working directory matters:
cd build/tests && ./tests "~[notCI]"
Install
sudo cmake --install build
With the default prefix this puts binaries in /usr/local/bin, the web files
in /usr/local/share/zoneminder/www, CGI in
/usr/local/libexec/zoneminder/cgi-bin, configuration in
/usr/local/etc/zm and the Perl modules in /Library/Perl/<version>.
Create the runtime directories
Nothing creates these for you. On Linux the distribution package does it; here you do it yourself, once:
sudo mkdir -p /usr/local/var/run/zm \
/usr/local/var/log/zm \
/usr/local/var/cache/zoneminder/temp \
/usr/local/var/lib/zoneminder/events
sudo chown -R _www:_www /usr/local/var/run/zm \
/usr/local/var/log/zm \
/usr/local/var/cache/zoneminder \
/usr/local/var/lib/zoneminder
_www is the account macOS runs its web server as, and is what configure
detects. Mapped memory files live in /usr/local/var/run/zm rather than
/dev/shm, which macOS does not have. That directory is on disk, not a RAM
filesystem — macOS mounts no tmpfs — so expect more disk traffic than the same
setup on Linux.
Database
brew services start mariadb
mysql -u root < /usr/local/share/zoneminder/db/zm_create.sql
mysql -u root -e "CREATE USER IF NOT EXISTS 'zmuser'@localhost IDENTIFIED BY 'zmpass';"
mysql -u root -e "GRANT LOCK TABLES, ALTER, SELECT, INSERT, UPDATE, DELETE, CREATE, INDEX ON zm.* TO 'zmuser'@localhost;"
sudo zmupdate.pl --nointeractive
Change the user and password from the defaults, in the grant above and in
/usr/local/etc/zm/zm.conf, before putting this anywhere reachable.
Web server
The build generates a starting point for both Apache and nginx at
build/misc/apache.conf and build/misc/nginx.conf, with your configured
paths already substituted. They are guidance, not drop-in configuration —
neither is installed, and both need adapting to how your web server is set up.
Whichever you choose, it needs PHP, CGI enabled for nph-zms, and rewrite
rules for the API. The sample files show all three.
Starting ZoneMinder
zmpkg.pl falls back to zmdc.pl when systemd is absent, so you can start
and stop ZoneMinder by hand:
sudo zmpkg.pl start
sudo zmpkg.pl status
sudo zmpkg.pl stop
To start it at boot, the build generates a launchd job at
build/misc/com.zoneminder.zoneminder.plist with your paths and web user
already filled in. Copy it into place and load it:
sudo install -o root -g wheel -m 644 \
build/misc/com.zoneminder.zoneminder.plist /Library/LaunchDaemons/
sudo launchctl load -w /Library/LaunchDaemons/com.zoneminder.zoneminder.plist
The job starts ZoneMinder; it does not supervise it. zmpkg.pl forks
zmdc.pl and returns — the same shape systemd calls Type=forking — and
from there zmdc.pl and zmwatch.pl restart the capture and analysis
daemons themselves. launchd’s job is to run that once at boot.
Unloading does not stop ZoneMinder, because launchd has no equivalent of
ExecStop. Stop it first:
sudo zmpkg.pl stop
sudo launchctl unload -w /Library/LaunchDaemons/com.zoneminder.zoneminder.plist
There is also no equivalent of the systemd unit’s After= and Requires=.
launchd only orders jobs it manages itself, and a Homebrew MariaDB is not one of
them, so at boot ZoneMinder may start before the database is listening.
zmdc.pl retries, but zmdc.log is the place to look if monitors come up
unexpectedly idle.
Known gaps
No Homebrew formula, so no
brew servicesintegration; the launchd job above is loaded by hand.Installation is from source only.
No log rotation.
misc/logrotate.confis written for logrotate, which macOS does not use — it usesnewsyslog, and no configuration is provided.No local camera support, as described at the top.
libunwind,libVLCandlibVNCare not found by default. All three are optional; the first only affects backtrace detail in crash logs.Configure warns that it cannot find
arp-scanandip. Neither is fatal:brew install arp-scancovers the first, andipis a Linux tool that macOS has no equivalent of, so monitor probing is a little less capable.