Zoneminder - Up and running in Docker with Nginx
This guide will hopefully outline all of the steps to get Zoneminder up and running under Nginx in a Docker container.
Setting up the Docker image
We will start by modifying the dlandon/zoneminder image as it has integrated ES and ML support and is a a good base. In order to get ZM working with Nginx we need to create our own Dockerfile. Create a new folder and place all of the following files into it. When ready execute a docker build tagging it as you deem fit.
docker build -t your/zoneminder zoneminder-folder
List of files
- Dockerfile
- 06_set_php_time.sh
- 40_firstrun.sh
- entrypoint.sh
- default
- objectconfig.ini
- zmeventnotification.ini
- secrets.ini
Dockerfile
FROM dlandon/zoneminder.machine.learning
RUN apt update && \
apt -y upgrade -o Dpkg::Options::="--force-confold" && \
apt -y autoremove
RUN apt -y remove apache2 libapache2-mod-php7.4 && \
apt -y install mysql-client-8.0 nginx fcgiwrap spawn-fcgi && \
apt -y autoremove
# Uncomment these if you want Nvidia support
RUN apt -y install python3-opencv nvtop
ENV NVIDIA_VISIBLE_DEVICES all
ENV NVIDIA_DRIVER_CAPABILITIES video,compute,utility
COPY entrypoint.sh .
RUN chmod 755 entrypoint.sh
COPY 06_set_php_time.sh /etc/my_init.d/
COPY 40_firstrun.sh /etc/my_init.d/
RUN rm /etc/my_init.d/20_apt_update.sh
RUN chmod 755 /etc/my_init.d/*.sh
COPY secrets.ini /root/zmeventnotification/secrets.ini
COPY zmeventnotification.ini /root/zmeventnotification/zmeventnotification.ini
COPY objectconfig.ini /root/zmeventnotification/objectconfig.ini
COPY default /etc/nginx/sites-available
ENTRYPOINT ./entrypoint.sh
In the above script you will see I have added both 06_set_php_time.sh and 40_firstrun.sh and removed 20_apt_update.sh. The removal of 20_apt_update.sh is optional but it does save startup time and ensures your image doesn’t change magically breaking stuff that once worked. I have also added the entrypoint.sh to facilitate the use of environment variables by supporting _FILE and auto filling .ini files.
06_set_php_time.sh
Modified to include the $TZ environment variable
#!/bin/bash
#
# 06_set_php_time.sh
#
sed -i "s|^date.timezone =.*$|date.timezone = ${TZ}|" /etc/php/7.4/fpm/php.ini
40_firstrun.sh
This is a big script that basically sets up all of the configuration files to starts all services. I modified the copying of files to use a single function copy_file_from_root I also added inital schema setup zm_create.sql and changed the startup behvaiour to load Nginx instead of Apache. MySql is also optional based on the ZM_DB_HOST environment variable.
!/bin/bash
#
# 40_firstrun.sh
#
#
# Github URL for opencv zip file download.
# Current default is to pull the version 4.3.0 release.
#
# Search for config files, if they don't exist, create the default ones
if [ ! -d /config/conf ]; then
echo "Creating conf folder"
mkdir /config/conf
else
echo "Using existing conf folder"
fi
# Handle the zm.conf files
echo "Copying zm.conf to config folder"
mv /root/zm.conf /config/conf/zm.default
cp /etc/zm/conf.d/README /config/conf/README
# Copy custom 99-mysql.conf to /etc/zm/conf.d/
if [ -f /config/conf/99-mysql.conf ]; then
echo "Copy custom 99-mysql.conf to /etc/zm/conf.d/"
cp /config/conf/99-mysql.conf /etc/zm/conf.d/99-mysql.conf
fi
copy_file_from_root() {
local dest=/config
if [[ -n $2 && $2 != "" ]]; then
dest=/config/$2
fi
if [ -f /root/zmeventnotification/$1 ]; then
echo "Moving $1 to $dest/$1.default"
mv /root/zmeventnotification/$1 $dest/$1.default
if [[ ! -f $dest/$1 || ${CONF_OVERRIDE,,} == "yes" ]]; then
cp $dest/$1.default $dest/$1
else
echo "$dest/$1 already exists"
fi
else
echo "File $1 already moved"
fi
}
# Handle the zmeventnotification.ini file
copy_file_from_root zmeventnotification.ini
# Handle the secrets.ini file
copy_file_from_root secrets.ini
# Create opencv folder if it doesn't exist
if [ ! -d /config/opencv ]; then
echo "Creating opencv folder in config folder"
mkdir /config/opencv
fi
# Handle the opencv.sh file
copy_file_from_root opencv.sh opencv
# Handle the debug_opencv.sh file
if [ -f /root/zmeventnotification/debug_opencv.sh ]; then
echo "Moving debug_opencv.sh"
mv /root/zmeventnotification/debug_opencv.sh /config/opencv/debug_opencv.sh
else
echo "File debug_opencv.sh already moved"
fi
if [ ! -f /config/opencv/opencv_ok ]; then
echo "no" > /config/opencv/opencv_ok
fi
# Handle the zmeventnotification.pl
if [ -f /root/zmeventnotification/zmeventnotification.pl ]; then
echo "Moving the event notification server"
mv /root/zmeventnotification/zmeventnotification.pl /usr/bin
chmod +x /usr/bin/zmeventnotification.pl 2>/dev/null
else
echo "Event notification server already moved"
fi
# Handle the pushapi_pushover.py
if [ -f /root/zmeventnotification/pushapi_pushover.py ]; then
echo "Moving the pushover api"
mkdir -p /var/lib/zmeventnotification/bin/
mv /root/zmeventnotification/pushapi_pushover.py /var/lib/zmeventnotification/bin/
chmod +x /var/lib/zmeventnotification/bin/pushapi_pushover.py 2>/dev/null
else
echo "Pushover api already moved"
fi
# Handle the es_rules.json
copy_file_from_root es_rules.json
# Move ssmtp configuration if it doesn't exist
if [ ! -d /config/ssmtp ]; then
echo "Moving ssmtp to config folder"
cp -p -R /etc/ssmtp/ /config/
else
echo "Using existing ssmtp folder"
fi
# Move mysql database if it doesn't exit
if [ ! -d /config/mysql/mysql ]; then
echo "Moving mysql to config folder"
rm -rf /config/mysql
cp -p -R /var/lib/mysql /config/
else
echo "Using existing mysql database folder"
fi
# files and directories no longer exposed at config.
rm -rf /config/perl5/
rm -rf /config/zmeventnotification/
rm -rf /config/zmeventnotification.pl
rm -rf /config/skins
rm -rf /config/zm.conf
# Create Control folder if it doesn't exist and copy files into image
if [ ! -d /config/control ]; then
echo "Creating control folder in config folder"
mkdir /config/control
else
echo "Copy /config/control/ scripts to /usr/share/perl5/ZoneMinder/Control/"
cp /config/control/*.pm /usr/share/perl5/ZoneMinder/Control/ 2>/dev/null
chown root:root /usr/share/perl5/ZoneMinder/Control/* 2>/dev/null
chmod 644 /usr/share/perl5/ZoneMinder/Control/* 2>/dev/null
fi
# Copy conf files if there are any
if [ -d /config/conf ]; then
echo "Copy /config/conf/ scripts to /etc/zm/conf.d/"
cp /config/conf/*.conf /etc/zm/conf.d/ 2>/dev/null
chown root:root /etc/zm/conf.d* 2>/dev/null
chmod 640 /etc/conf.d/* 2>/dev/null
fi
echo "Creating symbolink links"
# security certificate keys
rm -rf /etc/ssl/zoneminder.crt
ln -sf /config/keys/cert.crt /etc/ssl/zoneminder.crt
rm -rf /etc/ssl/zoneminder.key
ln -sf /config/keys/cert.key /etc/ssl/zoneminder.key
mkdir -p /var/lib/zmeventnotification/push
mkdir -p /config/push
rm -rf /var/lib/zmeventnotification/push/tokens.txt
ln -sf /config/push/tokens.txt /var/lib/zmeventnotification/push/tokens.txt
ln -sf /config/es_rules.json /etc/zm/es_rules.json
# ssmtp
rm -r /etc/ssmtp
ln -s /config/ssmtp /etc/ssmtp
# mysql
rm -r /var/lib/mysql
ln -s /config/mysql /var/lib/mysql
# Set ownership for unRAID
PUID=${PUID:-99}
PGID=${PGID:-100}
usermod -o -u $PUID nobody
# Check if the group with GUID passed as environment variable exists and create it if not.
if ! getent group "$PGID" >/dev/null; then
groupadd -g "$PGID" env-provided-group
echo "Group with id: $PGID did not already exist, so we created it."
fi
usermod -g $PGID nobody
usermod -d /config nobody
# Set ownership for mail
usermod -a -G mail www-data
# Change some ownership and permissions
chown -R mysql:mysql /config/mysql
chown -R mysql:mysql /var/lib/mysql
chown -R $PUID:$PGID /config/conf
chmod 777 /config/conf
chmod 666 /config/conf/*
chown -R $PUID:$PGID /config/control
chmod 777 /config/control
chmod 666 -R /config/control/
chown -R $PUID:$PGID /config/ssmtp
chmod -R 777 /config/ssmtp
chown -R $PUID:$PGID /config/zmeventnotification.*
chmod 666 /config/zmeventnotification.*
chown -R $PUID:$PGID /config/secrets.ini
chmod 666 /config/secrets.ini
chown -R $PUID:$PGID /config/opencv
chmod 777 /config/opencv
chmod 666 /config/opencv/*
chown -R $PUID:$PGID /config/keys
chmod 777 /config/keys
chmod 666 /config/keys/*
chown -R www-data:www-data /config/push/
chown -R www-data:www-data /var/lib/zmeventnotification/
chmod +x /config/opencv/opencv.sh
chmod +x /config/opencv/debug_opencv.sh
chmod +x /config/opencv/opencv.sh.default
# Create events folder
if [ ! -d /var/cache/zoneminder/events ]; then
echo "Create events folder"
mkdir /var/cache/zoneminder/events
chown -R www-data:www-data /var/cache/zoneminder/events
chmod -R 777 /var/cache/zoneminder/events
else
echo "Using existing data directory for events"
# Check the ownership on the /var/cache/zoneminder/events directory
if [ `stat -c '%U:%G' /var/cache/zoneminder/events` != 'www-data:www-data' ]; then
echo "Correcting /var/cache/zoneminder/events ownership..."
chown -R www-data:www-data /var/cache/zoneminder/events
fi
# Check the permissions on the /var/cache/zoneminder/events directory
if [ `stat -c '%a' /var/cache/zoneminder/events` != '777' ]; then
echo "Correcting /var/cache/zoneminder/events permissions..."
chmod -R 777 /var/cache/zoneminder/events
fi
fi
# Create images folder
if [ ! -d /var/cache/zoneminder/images ]; then
echo "Create images folder"
mkdir /var/cache/zoneminder/images
chown -R www-data:www-data /var/cache/zoneminder/images
chmod -R 777 /var/cache/zoneminder/images
else
echo "Using existing data directory for images"
# Check the ownership on the /var/cache/zoneminder/images directory
if [ `stat -c '%U:%G' /var/cache/zoneminder/images` != 'www-data:www-data' ]; then
echo "Correcting /var/cache/zoneminder/images ownership..."
chown -R www-data:www-data /var/cache/zoneminder/images
fi
# Check the permissions on the /var/cache/zoneminder/images directory
if [ `stat -c '%a' /var/cache/zoneminder/images` != '777' ]; then
echo "Correcting /var/cache/zoneminder/images permissions..."
chmod -R 777 /var/cache/zoneminder/images
fi
fi
# Create temp folder
if [ ! -d /var/cache/zoneminder/temp ]; then
echo "Create temp folder"
mkdir /var/cache/zoneminder/temp
chown -R www-data:www-data /var/cache/zoneminder/temp
chmod -R 777 /var/cache/zoneminder/temp
else
echo "Using existing data directory for temp"
# Check the ownership on the /var/cache/zoneminder/temp directory
if [ `stat -c '%U:%G' /var/cache/zoneminder/temp` != 'www-data:www-data' ]; then
echo "Correcting /var/cache/zoneminder/temp ownership..."
chown -R www-data:www-data /var/cache/zoneminder/temp
fi
# Check the permissions on the /var/cache/zoneminder/temp directory
if [ `stat -c '%a' /var/cache/zoneminder/temp` != '777' ]; then
echo "Correcting /var/cache/zoneminder/temp permissions..."
chmod -R 777 /var/cache/zoneminder/temp
fi
fi
# Create cache folder
if [ ! -d /var/cache/zoneminder/cache ]; then
echo "Create cache folder"
mkdir /var/cache/zoneminder/cache
chown -R www-data:www-data /var/cache/zoneminder/cache
chmod -R 777 /var/cache/zoneminder/cache
else
echo "Using existing data directory for cache"
# Check the ownership on the /var/cache/zoneminder/cache directory
if [ `stat -c '%U:%G' /var/cache/zoneminder/cache` != 'www-data:www-data' ]; then
echo "Correcting /var/cache/zoneminder/cache ownership..."
chown -R www-data:www-data /var/cache/zoneminder/cache
fi
# Check the permissions on the /var/cache/zoneminder/cache directory
if [ `stat -c '%a' /var/cache/zoneminder/cache` != '777' ]; then
echo "Correcting /var/cache/zoneminder/cache permissions..."
chmod -R 777 /var/cache/zoneminder/cache
fi
fi
# set user crontab entries
crontab -r -u root
if [ -f /config/cron ]; then
crontab -l -u root | cat - /config/cron | crontab -u root -
fi
# Symbolink for /config/zmeventnotification.ini
ln -sf /config/zmeventnotification.ini /etc/zm/zmeventnotification.ini
chown www-data:www-data /etc/zm/zmeventnotification.ini
# Symbolink for /config/secrets.ini
ln -sf /config/secrets.ini /etc/zm/
# Move the yolo models to /var/lib/zmeventnotification
if [ -d /root/models ]; then
mv /root/models/ /var/lib/zmeventnotification/
chown -R www-data:www-data /var/lib/zmeventnotification/models
fi
# Create hook folder
if [ ! -d /config/hook ]; then
echo "Creating /config/hook folder"
mkdir /config/hook
fi
# Handle the objectconfig.ini file
copy_file_from_root objectconfig.ini hook
# Handle the config_upgrade script
copy_file_from_root config_upgrade.py hook
# Handle the zm_event_start.sh file
copy_file_from_root zm_event_start.sh hook
# Handle the zm_event_end.sh file
copy_file_from_root zm_event_end.sh hook
# Handle the zm_detect.py file
copy_file_from_root zm_detect.py hook
# Handle the zm_detect_old.py file
copy_file_from_root zm_detect_old.py hook
# Handle the zm_train_faces.py file
copy_file_from_root zm_train_faces.py hook
# Handle the train_faces.py file
copy_file_from_root train_faces.py hook
# Symbolic link for known_faces in /config
rm -rf /var/lib/zmeventnotification/known_faces
ln -sf /config/hook/known_faces /var/lib/zmeventnotification/known_faces
chown -R www-data:www-data /var/lib/zmeventnotification/known_faces
# Symbolic link for unknown_faces in /config
rm -rf /var/lib/zmeventnotification/unknown_faces
ln -sf /config/hook/unknown_faces /var/lib/zmeventnotification/unknown_faces
chown -R www-data:www-data /var/lib/zmeventnotification/unknown_faces
# Symbolic link for misc in /config
rm -rf /var/lib/zmeventnotification/misc
ln -sf /config/hook/misc /var/lib/zmeventnotification/misc
chown -R www-data:www-data /var/lib/zmeventnotification/misc
# Create misc folder if it doesn't exist
if [ ! -d /config/hook/misc ]; then
echo "Creating hook/misc folder in config folder"
mkdir -p /config/hook/misc
fi
# Create coral_edgetpu folder if it doesn't exist
if [ ! -d /config/hook/coral_edgetpu ]; then
echo "Creating hook/coral_edgetpu folder in config folder"
mkdir -p /config/hook/coral_edgetpu
fi
# Symbolic link for coral_edgetpu in /config
rm -rf /var/lib/zmeventnotification/models/coral_edgetpu
ln -sf /config/hook/coral_edgetpu/ /var/lib/zmeventnotification/models
chown -R www-data:www-data /var/lib/zmeventnotification/models/coral_edgetpu 2>/dev/null
# Symbolic link for hook files in /config
mkdir -p /var/lib/zmeventnotification/bin
ln -sf /config/hook/zm_detect.py /var/lib/zmeventnotification/bin/zm_detect.py
ln -sf /config/hook/zm_detect_old.py /var/lib/zmeventnotification/bin/zm_detect_old.py
ln -sf /config/hook/zm_train_faces.py /var/lib/zmeventnotification/bin/zm_train_faces.py
ln -sf /config/hook/train_faces.py /var/lib/zmeventnotification/bin/train_faces.py
ln -sf /config/hook/zm_event_start.sh /var/lib/zmeventnotification/bin/zm_event_start.sh
ln -sf /config/hook/zm_event_end.sh /var/lib/zmeventnotification/bin/zm_event_end.sh
chmod +x /var/lib/zmeventnotification/bin/*
ln -sf /config/hook/objectconfig.ini /etc/zm/
# Create known_faces folder if it doesn't exist
if [ ! -d /config/hook/known_faces ]; then
echo "Creating hook/known_faces folder in config folder"
mkdir -p /config/hook/known_faces
fi
# Create unknown_faces folder if it doesn't exist
if [ ! -d /config/hook/unknown_faces ]; then
echo "Creating hook/unknown_faces folder in config folder"
mkdir -p /config/hook/unknown_faces
fi
# Set hook folder permissions
chown -R $PUID:$PGID /config/hook
chmod -R 777 /config/hook
# Compile opencv
if [ -f /config/opencv/opencv_ok ] && [ `cat /config/opencv/opencv_ok` = 'yes' ]; then
if [ ! -f /root/setup.py ]; then
if [ -x /config/opencv/opencv.sh ]; then
/config/opencv/opencv.sh quiet &>/dev/null
fi
fi
fi
mv /root/zmeventnotification/setup.py /root/setup.py 2>/dev/null
# Clean up mysql log files to insure mysql will start
rm -f /config/mysql/ib_logfile* 2>/dev/null
if [[ -n $ZM_DB_HOST && $ZM_DB_HOST != 'localhost' ]]
then
mysql -u $ZM_DB_USER -p$ZM_DB_PASS -D $ZM_DB_NAME -h $ZM_DB_HOST -e 'SELECT COUNT(1) FROM zm.Config' > /dev/null 2>&1
if [[ $? -ne 0 ]]
then
if [[ -z ${ZM_DB_ROOT_PASS} ]]
then
echo "Cannot create schema, root user not defined"
exit 1
else
echo "Attempting to create schema"
mysql -u root -p$ZM_DB_ROOT_PASS -D $ZM_DB_NAME -h $ZM_DB_HOST < /usr/share/zoneminder/db/zm_create.sql > /dev/null 2>&1
mysql -u root -p$ZM_DB_ROOT_PASS -D $ZM_DB_NAME -h $ZM_DB_HOST -e "GRANT ALL PRIVILEGES ON zm.* TO '${ZM_DB_USER}'@'%'" > /dev/null 2>&1
fi
fi
fi
if [ "$NO_START_ZM" != "1" ]; then
echo "Starting services..."
service nginx start
service php7.4-fpm start
service fcgiwrap start
if [[ -z $ZM_DB_HOST || $ZM_DB_HOST == 'localhost' ]]
then
# Start mysql
service mysql start
sleep 3
fi
# Update the database if necessary
zmupdate.pl -nointeractive
zmupdate.pl -f
service zoneminder start
else
echo "Services not started..."
fi
entrypoint.sh
The primary entrypoint for the docker container. This script will auto-load _FILE environment secrets and prepopulate the zm.conf and secrets.ini files.
#!/bin/bash
file_env() {
local var="$1"
local fileVar="${var}_FILE"
local def="${2:-}"
if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
echo "Both $var and $fileVar are set (but are exclusive)"
fi
local val="$def"
if [ "${!var:-}" ]; then
val="${!var}"
elif [ "${!fileVar:-}" ]; then
val="$(< "${!fileVar}")"
fi
export "$var"="$val"
unset "$fileVar"
}
for i in `env | grep _FILE= | cut -f1 -d'=' | sed -e "s/_FILE//g"`
do
file_env $i
done
update_file() {
for i in `env | grep $1 | grep -v _FILE`
do
NAME=`echo $i | cut -f1 -d'='`;
VALUE=`echo $i | cut -f2- -d'='`;
VALUE=${VALUE//\//\\/};
VALUE=${VALUE//&/\\&};
sed -i "s/${NAME}=.*/${NAME}=${VALUE}/g" $2;
done
}
echo "Updating zm.conf"
update_file ZM_ /etc/zm/zm.conf
if [ -f /root/zmeventnotification/secrets.ini ]; then
echo "Updating secrets.ini"
update_file ES_ /root/zmeventnotification/secrets.ini
update_file ML_ /root/zmeventnotification/secrets.ini
fi
/sbin/my_init
default
This is the default Nginx site. It is slightly modified from the original Apache site insofar as it runs from / instead of /zm with the only exception being /zm/cgi-bin which is hardcoded in index.php. I did not implement SSL here as this container will not be exposed outside of the docker network.
server {
listen 80;
listen [::]:80;
add_header Access-Control-Allow-Origin *;
root /usr/share/zoneminder/www;
index index.php;
location ~ \.php$ {
if (!-f $request_filename) { return 404; }
expires epoch;
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME $request_filename;
fastcgi_index index.php;
fastcgi_intercept_errors on;
fastcgi_pass unix:/run/php/php7.4-fpm.sock;
try_files $uri $uri/ /index.php;
}
location ~ \.(jpg|jpeg|gif|png|ico)$ {
access_log off;
expires 33d;
}
location /api/ {
rewrite ^/api(.+)$ /api/index.php?p=$1 last;
}
location /api/css {
alias /usr/share/zoneminder/www/api/app/webroot/css;
}
location /zm/cgi-bin {
gzip off;
alias /usr/lib/zoneminder/cgi-bin;
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME $request_filename;
fastcgi_intercept_errors on;
fastcgi_pass unix:/run/fcgiwrap.socket;
}
location /cache {
alias /var/cache/zoneminder/cache;
}
}
objectconfig.ini
This is the configuration file for object detection, it was slightly modified to pull additional configurations from the secrets.ini
# Configuration file for object detection
# NOTE: ALL parameters here can be overriden
# on a per monitor basis if you want. Just
# duplicate it inside the correct [monitor-<num>] section
# You can create your own custom attributes in the [custom] section
[general]
# Please don't change this. It is used by the config upgrade script
version=1.2
# You can now limit the # of detection process
# per target processor. If not specified, default is 1
# Other detection processes will wait to acquire lock
cpu_max_processes=3
tpu_max_processes=1
gpu_max_processes=1
# Time to wait in seconds per processor to be free, before
# erroring out. Default is 120 (2 mins)
cpu_max_lock_wait=100
tpu_max_lock_wait=100
gpu_max_lock_wait=100
#pyzm_overrides={'conf_path':'/etc/zm','log_level_debug':0}
pyzm_overrides={'log_level_debug':5}
# This is an optional file
# If specified, you can specify tokens with secret values in that file
# and onlt refer to the tokens in your main config file
secrets = /etc/zm/secrets.ini
# portal/user/password are needed if you plan on using ZM's legacy
# auth mechanism to get images
portal=!ES_ZM_PORTAL
user=!ES_ZM_USER
password=!ES_ZM_PASSWORD
# api portal is needed if you plan to use tokens to get images
# requires ZM 1.33 or above
api_portal=!ES_ZM_API_PORTAL
allow_self_signed=yes
# if yes, last detection will be stored for monitors
# and bounding boxes that match, along with labels
# will be discarded for new detections. This may be helpful
# in getting rid of static objects that get detected
# due to some motion.
match_past_detections=no
# The max difference in area between the objects if match_past_detection is on
# can also be specified in px like 300px. Default is 5%. Basically, bounding boxes of the same
# object can slightly differ ever so slightly between detection. Contributor @neillbell put in this PR
# to calculate the difference in areas and based on his tests, 5% worked well. YMMV. Change it if needed.
# Note: You can specify label/object specific max_diff_areas as well. If present, they override this value
# example:
# person_past_det_max_diff_area=5%
# car_past_det_max_diff_area=5000px
past_det_max_diff_area=5%
# this is the maximum size a detected object can have. You can specify it in px or % just like past_det_max_diff_area
# This is pretty useful to eliminate bogus detection. In my case, depending on shadows and other lighting conditions,
# I sometimes see "car" or "person" detected that covers most of my driveway view. That is practically impossible
# and therefore I set mine to 70% because I know any valid detected objected cannot be larger than that area
max_detection_size=90%
# sequence of models to run for detection
detection_sequence=object,face,alpr
# if all, then we will loop through all models
# if first then the first success will break out
detection_mode=all
# If you need basic auth to access ZM
#basic_user=user
#basic_password=password
# base data path for various files the ES+OD needs
# we support in config variable substitution as well
base_data_path=/var/lib/zmeventnotification
# global settings for
# bestmatch, alarm, snapshot OR a specific frame ID
frame_id=bestmatch
# this is the to resize the image before analysis is done
resize=800
# set to yes, if you want to remove images after analysis
# setting to yes is recommended to avoid filling up space
# keep to no while debugging/inspecting masks
# Note this does NOT delete debug images later
delete_after_analyze=yes
# If yes, will write an image called <filename>-bbox.jpg as well
# which contains the bounding boxes. This has NO relation to
# write_image_to_zm
# Typically, if you enable delete_after_analyze you may
# also want to set write_debug_image to no.
write_debug_image=no
# if yes, will write an image with bounding boxes
# this needs to be yes to be able to write a bounding box
# image to ZoneMinder that is visible from its console
write_image_to_zm=yes
# Adds percentage to detections
# hog/face shows 100% always
show_percent=yes
# color to be used to draw the polygons you specified
poly_color=(255,255,255)
poly_thickness=2
#import_zm_zones=yes
only_triggered_zm_zones=no
# This section gives you an option to get brief animations
# of the event, delivered as part of the push notification to mobile devices
# Animations are created only if an object is detected
#
# NOTE: This will DELAY the time taken to send you push notifications
# It will try to first creat the animation, which may take upto a minute
# depending on how soon it gets access to frames. See notes below
[animation]
# If yes, object detection will attempt to create
# a short GIF file around the object detection frame
# that can be sent via push notifications for instant playback
# Note this required additional software support. Default:no
create_animation=no
# Format of animation burst
# valid options are "mp4", "gif", "mp4,gif"
# Note that gifs will be of a shorter duration
# as they take up much more disk space than mp4
animation_types='mp4,gif'
# default width of animation image. Be cautious when you increase this
# most mobile platforms give a very brief amount of time (in seconds)
# to download the image.
# Given your ZM instance will be serving the image, it will anyway be slow
# Making the total animation size bigger resulted in the notification not
# getting an image at all (timed out)
animation_width=640
# When an event is detected, ZM it writes frames a little late
# On top of that, it looks like with caching enabled, the API layer doesn't
# get access to DB records for much longer (around 30 seconds), at least on my
# system. animation_retry_sleep refers to how long to wait before trying to grab
# frame information if it failed. animation_max_tries defines how many times it
# will try and retrieve frames before it gives up
animation_retry_sleep=15
animation_max_tries=4
# if animation_types is gif then when can generate a fast preview gif
# every second frame is skipped and the frame rate doubled
# to give quick preview, Default (no)
fast_gif=no
[remote]
# You can now run the machine learning code on a different server
# This frees up your ZM server for other things
# To do this, you need to setup https://github.com/pliablepixels/mlapi
# on your desired server and confiure it with a user. See its instructions
# once set up, you can choose to do object/face recognition via that
# external serer
# URL that will be used
#ml_gateway=!ML_GATEWAY
#ml_gateway=http://192.168.1.183:5000/api/v1
#ml_gateway=http://10.6.1.13:5000/api/v1
#ml_gateway=http://192.168.1.21:5000/api/v1
#ml_gateway=http://10.9.0.2:5000/api/v1
#ml_fallback_local=yes
# API/password for remote gateway
ml_user=!ML_USER
ml_password=!ML_PASSWORD
# config for object
[object]
# If you are using legacy format (use_sequence=no) then these parameters will
# be used during ML inferencing
object_detection_pattern=(person|car|motorbike|bus|truck|boat)
object_min_confidence=0.3
object_framework=coral_edgetpu
object_processor=tpu
object_weights={{base_data_path}}/models/coral_edgetpu/ssd_mobilenet_v2_coco_quant_postprocess_edgetpu.tflite
object_labels={{base_data_path}}/models/coral_edgetpu/coco_indexed.names
# If you are using the new ml_sequence format (use_sequence=yes) then
# you can fiddle with these parameters and look at ml_sequence later
# Note that these can be named anything. You can add custom variables, ad-infinitum
# Google Coral
# The mobiledet model came out in Nov 2020 and is supposed to be faster and more accurate but YMMV
tpu_object_weights_mobiledet={{base_data_path}}/models/coral_edgetpu/ssdlite_mobiledet_coco_qat_postprocess_edgetpu.tflite
tpu_object_weights_mobilenet={{base_data_path}}/models/coral_edgetpu/ssd_mobilenet_v2_coco_quant_postprocess_edgetpu.tflite
tpu_object_labels={{base_data_path}}/models/coral_edgetpu/coco_indexed.names
tpu_object_framework=coral_edgetpu
tpu_object_processor=tpu
tpu_min_confidence=0.6
# Yolo v4 on GPU (falls back to CPU if no GPU)
yolo4_object_weights={{base_data_path}}/models/yolov4/yolov4.weights
yolo4_object_labels={{base_data_path}}/models/yolov4/coco.names
yolo4_object_config={{base_data_path}}/models/yolov4/yolov4.cfg
yolo4_object_framework=opencv
yolo4_object_processor=gpu
# Yolo v3 on GPU (falls back to CPU if no GPU)
yolo3_object_weights={{base_data_path}}/models/yolov3/yolov3.weights
yolo3_object_labels={{base_data_path}}/models/yolov3/coco.names
yolo3_object_config={{base_data_path}}/models/yolov3/yolov3.cfg
yolo3_object_framework=opencv
yolo3_object_processor=gpu
# Tiny Yolo V4 on GPU (falls back to CPU if no GPU)
tinyyolo_object_config={{base_data_path}}/models/tinyyolov4/yolov4-tiny.cfg
tinyyolo_object_weights={{base_data_path}}/models/tinyyolov4/yolov4-tiny.weights
tinyyolo_object_labels={{base_data_path}}/models/tinyyolov4/coco.names
tinyyolo_object_framework=opencv
tinyyolo_object_processor=gpu
[face]
face_detection_pattern=.*
known_images_path={{base_data_path}}/known_faces
unknown_images_path={{base_data_path}}/unknown_faces
save_unknown_faces=yes
save_unknown_faces_leeway_pixels=100
face_detection_framework=dlib
# read https://github.com/ageitgey/face_recognition/wiki/Face-Recognition-Accuracy-Problems
# read https://github.com/ageitgey/face_recognition#automatically-find-all-the-faces-in-an-image
# and play around
# quick overview:
# num_jitters is how many times to distort images
# upsample_times is how many times to upsample input images (for small faces, for example)
# model can be hog or cnn. cnn may be more accurate, but I haven't found it to be
face_num_jitters=1
face_model=cnn
face_upsample_times=1
# This is maximum distance of the face under test to the closest matched
# face cluster. The larger this distance, larger the chances of misclassification.
#
face_recog_dist_threshold=0.6
# When we are first training the face recognition model with known faces,
# by default we use hog because we assume you will supply well lit, front facing faces
# However, if you are planning to train with profile photos or hard to see faces, you
# may want to change this to cnn. Note that this increases training time, but training only
# happens once, unless you retrain again by removing the training model
face_train_model=cnn
#if a face doesn't match known names, we will detect it as 'unknown face'
# you can change that to something that suits your personality better ;-)
#unknown_face_name=invader
[alpr]
alpr_detection_pattern=.*
alpr_use_after_detection_only=yes
# Many of the ALPR providers offer both a cloud version
# and local SDK version. Sometimes local SDK format differs from
# the cloud instance. Set this to local or cloud. Default cloud
alpr_api_type=cloud
# -----| If you are using plate recognizer | ------
alpr_service=plate_recognizer
#alpr_service=open_alpr_cmdline
# If you want to host a local SDK https://app.platerecognizer.com/sdk/
#alpr_url=http://192.168.1.21:8080/alpr
# Plate recog replace with your api key
alpr_key=!ML_PLATEREC_ALPR_KEY
# if yes, then it will log usage statistics of the ALPR service
platerec_stats=yes
# If you want to specify regions. See http://docs.platerecognizer.com/#regions-supported
#platerec_regions=['us','cn','kr']
# minimal confidence for actually detecting a plate
platerec_min_dscore=0.1
# minimal confidence for the translated text
platerec_min_score=0.2
# ----| If you are using openALPR |-----
#alpr_service=open_alpr
#alpr_key=!ML_OPENALPR_ALPR_KEY
# For an explanation of params, see http://doc.openalpr.com/api/?api=cloudapi
#openalpr_recognize_vehicle=1
#openalpr_country=us
#openalpr_state=ca
# openalpr returns percents, but we convert to between 0 and 1
#openalpr_min_confidence=0.3
# ----| If you are using openALPR command line |-----
openalpr_cmdline_binary=alpr
# Do an alpr -help to see options, plug them in here
# like say '-j -p ca -c US' etc.
# keep the -j because its JSON
# Note that alpr_pattern is honored
# For the rest, just stuff them in the cmd line options
openalpr_cmdline_params=-j -d
openalpr_cmdline_min_confidence=0.3
## Monitor specific settings
# Examples:
# Let's assume your monitor ID is 999
[monitor-999]
# my driveway
match_past_detections=no
wait=5
object_detection_pattern=(person)
# Advanced example - here we want anything except potted plant
# exclusion in regular expressions is not
# as straightforward as you may think, so
# follow this pattern
# object_detection_pattern = ^(?!object1|object2|objectN)
# the characters in front implement what is
# called a negative look ahead
# object_detection_pattern=^(?!potted plant|pottedplant|bench|broccoli)
#alpr_detection_pattern=^(.*x11)
#delete_after_analyze=no
#detection_pattern=.*
#import_zm_zones=yes
# polygon areas where object detection will be done.
# You can name them anything except the keywords defined in the optional
# params below. You can put as many polygons as you want per [monitor-<mid>]
# (see examples).
my_driveway=306,356 1003,341 1074,683 154,715
# You are now allowed to specify detection pattern per zone
# the format is <polygonname>_zone_detection_pattern=<regexp>
# So if your polygon is called my_driveway, its associated
# detection pattern will be my_driveway_zone_detection_pattern
# If none is specified, the value in object_detection_pattern
# will be used
# This also applies to ZM zones. Let's assume you have
# import_zm_zones=yes and let's suppose you have a zone in ZM
# called Front_Door. In that case, all you need to do is put in a
# front_door_zone_detection_pattern=(person|car) here
#
# NOTE: ZM Zones are converted to lowercase, and spaces are replaced
# with underscores@3
my_driveway_zone_detection_pattern=(person)
some_other_area=0,0 200,300 700,900
# use license plate recognition for my driveway
# see alpr section later for more data needed
resize=no
detection_sequence=object,alpr
[ml]
# When enabled, you can specify complex ML inferencing logic in ml_sequence
# Anything specified in ml_sequence will override any other ml attributes
# Also, when enabled, stream_sequence will override any other frame related
# attributes
use_sequence = yes
# if enabled, will not grab exclusive locks before running inferencing
# locking seems to cause issues on some unique file systems
disable_locks= no
# Chain of frames
# See https://zmeventnotification.readthedocs.io/en/latest/guides/hooks.html#understanding-detection-configuration
# Also see https://pyzm.readthedocs.io/en/latest/source/pyzm.html#pyzm.ml.detect_sequence.DetectSequence.detect_stream
# Very important: Make sure final ending brace is indented
stream_sequence = {
'frame_strategy': 'most_models',
'frame_set': 'snapshot,alarm',
'contig_frames_before_error': 5,
'max_attempts': 3,
'sleep_between_attempts': 4,
'resize':800
}
# Chain of ML models to use
# See https://zmeventnotification.readthedocs.io/en/latest/guides/hooks.html#understanding-detection-configuration
# Also see https://pyzm.readthedocs.io/en/latest/source/pyzm.html#pyzm.ml.detect_sequence.DetectSequence
# Very important: Make sure final ending brace is indented
ml_sequence= {
'general': {
'model_sequence': 'object,face,alpr',
'disable_locks': '{{disable_locks}}',
'match_past_detections': '{{match_past_detections}}',
'past_det_max_diff_area': '5%',
'car_past_det_max_diff_area': '10%',
#'ignore_past_detection_labels': ['dog', 'cat']
},
'object': {
'general':{
'pattern':'{{object_detection_pattern}}',
'same_model_sequence_strategy': 'first' # also 'most', 'most_unique's
},
'sequence': [{
#First run on TPU with higher confidence
'name': 'TPU object detection',
'enabled': 'no',
'object_weights':'{{tpu_object_weights_mobiledet}}',
'object_labels': '{{tpu_object_labels}}',
'object_min_confidence': {{tpu_min_confidence}},
'object_framework':'{{tpu_object_framework}}',
'tpu_max_processes': {{tpu_max_processes}},
'tpu_max_lock_wait': {{tpu_max_lock_wait}},
'max_detection_size':'{{max_detection_size}}'
},
{
# YoloV4 on GPU if TPU fails (because sequence strategy is 'first')
'name': 'YoloV4 GPU/CPU',
'enabled': 'yes', # don't really need to say this explictly
'object_config':'{{yolo4_object_config}}',
'object_weights':'{{yolo4_object_weights}}',
'object_labels': '{{yolo4_object_labels}}',
'object_min_confidence': {{object_min_confidence}},
'object_framework':'{{yolo4_object_framework}}',
'object_processor': '{{yolo4_object_processor}}',
'gpu_max_processes': {{gpu_max_processes}},
'gpu_max_lock_wait': {{gpu_max_lock_wait}},
'cpu_max_processes': {{cpu_max_processes}},
'cpu_max_lock_wait': {{cpu_max_lock_wait}},
'max_detection_size':'{{max_detection_size}}'
}]
},
'face': {
'general':{
'pattern': '{{face_detection_pattern}}',
'same_model_sequence_strategy': 'union' # combines all outputs of this sequence
},
'sequence': [
{
'name': 'TPU face detection',
'enabled': 'no',
'face_detection_framework': 'tpu',
'face_weights':'/var/lib/zmeventnotification/models/coral_edgetpu/ssd_mobilenet_v2_face_quant_postprocess_edgetpu.tflite',
'face_min_confidence': 0.3,
},
{
'name': 'DLIB based face recognition',
'enabled': 'yes',
#'pre_existing_labels': ['face'], # If you use TPU detection first, we can run this ONLY if TPU detects a face first
'save_unknown_faces':'{{save_unknown_faces}}',
'save_unknown_faces_leeway_pixels':{{save_unknown_faces_leeway_pixels}},
'face_detection_framework': '{{face_detection_framework}}',
'known_images_path': '{{known_images_path}}',
'unknown_images_path': '{{unknown_images_path}}',
'face_model': '{{face_model}}',
'face_train_model': '{{face_train_model}}',
'face_recog_dist_threshold': '{{face_recog_dist_threshold}}',
'face_num_jitters': '{{face_num_jitters}}',
'face_upsample_times':'{{face_upsample_times}}',
'gpu_max_processes': {{gpu_max_processes}},
'gpu_max_lock_wait': {{gpu_max_lock_wait}},
'cpu_max_processes': {{cpu_max_processes}},
'cpu_max_lock_wait': {{cpu_max_lock_wait}},
'max_size':800
}]
},
'alpr': {
'general':{
'same_model_sequence_strategy': 'first',
'pre_existing_labels':['car', 'motorbike', 'bus', 'truck', 'boat'],
'pattern': '{{alpr_detection_pattern}}'
},
'sequence': [{
'name': 'Platerecognizer cloud',
'enabled': 'yes',
'alpr_api_type': '{{alpr_api_type}}',
'alpr_service': '{{alpr_service}}',
'alpr_key': '{{alpr_key}}',
'platrec_stats': '{{platerec_stats}}',
'platerec_min_dscore': {{platerec_min_dscore}},
'platerec_min_score': {{platerec_min_score}},
'max_size':1600,
#'platerec_payload': {
#'regions':['us'],
#'camera_id':12,
#},
#'platerec_config': {
# 'region':'strict',
# 'mode': 'fast'
#}
}]
}
}
zmeventnotification.ini
This is the configuration file for the event server, it was slightly modified to pull additional configurations from the secrets.ini
# Configuration file for zmeventnotification.pl
[general]
secrets = /etc/zm/secrets.ini
base_data_path=/var/lib/zmeventnotification
# The ES now supports a means for a special kind of
# websocket connection which can dynamically control ES
# behaviour
# Default is no
use_escontrol_interface=!ES_USE_INTERFACE
# this is where all escontrol admin overrides
# will be stored.
escontrol_interface_file=/var/lib/zmeventnotification/misc/escontrol_interface.dat
# the password for accepting control interfaces
escontrol_interface_password=!ES_CONTROL_INTERFACE_PASSWORD
# If you see the ES getting 'stuck' after several hours
# see https://rt.cpan.org/Public/Bug/Display.html?id=131058
# You can use restart_interval to have it automatically restart
# every X seconds. (Default is 7200 = 2 hours) Set to 0 to disable this.
# restart_interval = 432000
restart_interval = 0
# list of monitors which ES will ignore
# Note that there is an attribute later that does
# not process hooks for specific monitors. This one is different
# It can be used to completely skip ES processing for the
# monitors defined
# skip_monitors = 2,3,4
[network]
# Port for Websockets connection (default: 9000).
port = 9000
[auth]
# Check username/password against ZoneMinder database (default: yes).
enable = yes
# Authentication timeout, in seconds (default: 20).
timeout = 20
[push]
# This is to enable sending push notifications via any 3rd party service.
# Typically, if you enable this, you might want to turn off fcm
# Note that zmNinja will only receive notifications via FCM, but other 3rd
# party services have their own apps to get notifications
use_api_push = no
# This is the script that will send the notification
# Some sample scripts are provided, write your own
# Each script gets:
# arg1 - event ID
# arg2 - Monitor ID
# arg3 - Monitor Name
# arg4 - alarm cause
# arg5 - Type of event (event_start or event_end)
# arg6 (optional) - image path
api_push_script=/var/lib/zmeventnotification/bin/pushapi_pushover.py
[fcm]
# Use FCM for messaging (default: yes).
enable = yes
# Use the new FCM V1 protocol (recommended)
use_fcmv1 = yes
# if yes, will replace notifications with the latest one
# default: no
replace_push_messages = no
# Custom FCM API key. Uncomment if you are using
# your own API key (most people will not need to uncomment)
# api_key =
# Auth token store location (default: /var/lib/zmeventnotification/push/tokens.txt).
token_file = {{base_data_path}}/push/tokens.txt
# Date format to use when sending notification
# over push (FCM)
# See https://metacpan.org/pod/POSIX::strftime::GNU
# For example, a 24 hr format would be
#date_format = %H:%M, %d-%b
date_format = %I:%M %p, %d-%b
# Set priority for android push. Default is high.
# You can set it to high or normal.
# There is weird foo going on here. If you set it to high,
# and don't interact with push, users report after a while they
# get delayed by Google. I haven't quite figured out what is the precise
# value to put here to make sure it always reaches you. Also make sure
# you read the zmES faq on delayed push
fcm_android_priority = high
# If you see messages not being delivered in doze mode for android
# Even AFTER you disable battery optimization for the app, try making this 0
# otherwise leave it unspecified. The value here is in seconds
# it specifies how long the message will be valid before it is discarded
# Some reports say if you set this to 0, android will try and deliver it immediately
# while others say it won't. YMMV.
# fcm_android_ttl = 0
# Use MQTT for messaging (default: no)
[mqtt]
enable=!ES_MQTT_ENABLED
# Allow you to set a custom MQTT topic name
# default: zoneminder
#topic = my topic name
# MQTT server (default: 127.0.0.1)
server=!ES_MQTT_HOST
# Authenticate to MQTT server as user
username=!ES_MQTT_USERNAME
# Password
password=!ES_MQTT_PASSWORD
# Set retain flag on MQTT messages (default: no)
retain = no
# MQTT over TLS
# Location to MQTT broker CA certificate. Uncomment this line will enable MQTT over TLS.
# tls_ca = /config/certs/ca.pem
# To enable 2-ways TLS, add client certificate and private key
# Location to client certificate and private key
# tls_cert = /config/es-pub.pem
# tls_key = /config/es-key.pem
# To allow insecure TLS (disable peer verifier), (default: no)
# tls_insecure = yes
[ssl]
# Enable SSL (default: yes)
enable=!ES_SSL
cert=!ES_CERT_FILE
key=!ES_KEY_FILE
#cert = /etc/apache2/ssl/zoneminder.crt
#key = /etc/apache2/ssl/zoneminder.key
# Location to SSL cert (no default).
# cert = /etc/apache2/ssl/yourportal/zoneminder.crt
# Location to SSL key (no default).
# key = /etc/apache2/ssl/yourportal/zoneminder.key
[customize]
# Link to json file that has rules which can be customized
# es_rules=/etc/zm/es_rules.json
# Display messages to console (default: no).
# Note that you can keep this to no and just
# use --debug when running from CLI too
console_logs = no
# debug level for ES messages. Default 4. Note that this is
# not controllable by ZM LOG_DEBUG_LEVEL as in Perl, ZM doesn't
# support debug levels
es_debug_level = 4
# Interval, in seconds, after which we will check for new events (default: 5).
event_check_interval = 5
# Interval, in seconds, to reload known monitors (default: 300).
monitor_reload_interval = 300
# Read monitor alarm cause (Requires ZoneMinder >= 1.31.2, default: no)
# Enabling this to 1 for lower versions of ZM will result in a crash
read_alarm_cause = yes
# Tag event IDs with the alarm (default: no).
tag_alarm_event_id = yes
# Use custom notification sound (default: no).
use_custom_notification_sound = no
# include picture in alarm (default: no).
include_picture = yes
# send event start notifications (default: yes)
# If no, starting notifications will not be sent out
send_event_start_notification = yes
# send event end notifications (default: no)
# Note that if you are using hooks for end notifications, they may change
# the final decision. This needs to be yes if you want end notifications with
# or without hooks
send_event_end_notification = yes
# URL to access the event image
# This URL can be anything you want
# What I've put here is a way to extract an image with the highest score given an eventID (even one that is recording)
# This requires the latest version of index.php which was merged on Oct 9, 2018 and may only work in ZM 1.32+
# https://github.com/ZoneMinder/zoneminder/blob/master/web/index.php
# If you use this URL as I've specified below, keep the EVENTID phrase intact.
# The notification server will replace it with the correct eid of the alarm
# BESTMATCH should be used only if you are using bestmatch for FID in detect_wrapper.sh
# objdetect is ONLY available in ZM 1.33+
# objdetect_mp4 and objdetect_gif is ONLY available
# in ZM 1.35+
picture_url = !ES_ZMES_PICTURE_URL
picture_portal_username=!ES_ZM_USER
picture_portal_password=!ES_ZM_PASSWORD
# This is a master on/off setting for hooks. If it is set to no
# hooks will not be used no matter what is set in the [hook] section
# This makes it easy for folks not using hooks to just turn this off
# default:no
use_hooks = yes
[hook]
# NOTE: This entire section is only valid if use_hooks is yes above
# When a hook is invoked, the ES forks a child. If you are in a situation
# where your motion sensititivy in ZM is not set properly, you may land up
# triggering hundreds of child processes of zm_detect that may potentially
# crash your system. Note that there are global locks around the ML code which
# are controlled by xxx_max_processes in the objectconfig/mlapiconfig.files
# which will avoid parallel running of models. But this is if you are facing issues
# by the simple fact that too many zm_detect processes are forked (which will apply
# whether you use mlapi or not). While I do feel the core issue is that you need
# to fix your ZM sensitivity, this parameter helps control.
# NOTE: When you put in value for this, any hooks that attempt to kick off
# beyond this limit will simply be ignored. There is no queueing.
# A value of 0 (default) means there are no limits
max_parallel_hooks=0
# Shell script name here to be called every time an alarm is detected
# the script will get passed $1=alarmEventID, $2=alarmMonitorId
# $3 monitor Name, $4 alarm cause
# script needs to return 0 to send alarm (default: none)
#
# This script is called when an event first starts. If the script returns "0"
# (success), then a notification is sent to channels specified in
# event_start_notify_on_hook_success. If the script returns "1" (fail)
# then a notification is sent to channels specified in
# event_start_notify_on_hook_fail
event_start_hook = '{{base_data_path}}/bin/zm_event_start.sh'
#This script is called after event_start_hook completes. You can do
# your housekeeping work here
#event_start_hook_notify_userscript = '{{base_data_path}}/contrib/example.py'
# This script is called when an event ends. If the script returns "0"
# (success), then a notification is sent to channels specified in
# event_end_notify_on_hook_success. If the script returns "1" (fail)
# then a notification is sent to channels specified in
# event_end_notify_on_hook_fail
# event_end_hook = '{{base_data_path}}/bin/zm_event_end.sh'
#This script is called after event_end_hook completes. You can do
# your housekeeping work here
#event_end_hook_notify_userscript = '{{base_data_path}}/contrib/example.py'
# Possible channels = web,fcm,mqtt,api
# all is short for web,fcm,mqtt,api
# use none for no notifications, or comment out the attribute
# When an event starts and hook returns 0, send notification to all. Default: none
event_start_notify_on_hook_success = all
# When an event starts and hook returns 1, send notification only to desktop. Default: none
event_start_notify_on_hook_fail = none
# When an event ends and hook returns 0, send notification to fcm,web,api. Default: none
event_end_notify_on_hook_success = fcm,web,api
# When an event ends and hook returns 1, don't send notifications. Default: none
event_end_notify_on_hook_fail = none
#event_end_notify_on_hook_fail = web
# Since event_end and event_start are two different hooks, it is entirely possible
# that you can get an end notification but not a start notification. This can happen
# if your start script returns 1 but the end script returns 0, for example. To avoid
# this, set this to yes (default:yes)
event_end_notify_if_start_success = yes
# If yes, the text returned by the script
# overwrites the alarm header
# useful if your script is detecting people, for example
# and you want that to be shown in your notification (default:yes)
use_hook_description = yes
# If yes will will append an [a] for alarmed frame match
# [s] for snapshot match or [x] if not using bestmatch
# really only a debugging feature but useful to know
# where object detection is working or failing
keep_frame_match_type = yes
# list of monitors for which hooks will not run
# hook_skip_monitors = 2
# if enabled, will pass the right folder for the hook script
# to store the detected image, so it shows up in ZM console view too
# Requires ZM >=1.33. Don't enable this if you are running an older version
# Note: you also need to set write_image_to_zm=yes in objectconfig.ini
# default: no
hook_pass_image_path = yes
secrets.ini
This is the secrets file that will be prepopulated by entrypoint.sh and any environment variables set by docker-compose
# your secrets file
[secrets]
# fid can have the following values:
# a particular <frameid>, alarm or snapshot
# starting ZM 1.35, you can also specify
# objdetect_mp4, objdetect_gif or objdetect_image
# this needs create_animation enabled in objectconfig.ini and associated flags
# If you keep it to objdetect, if you created a GIF file in objectconfig, then
# a GIF file will be used else an image. If you opted for MP4 in objectconfig,
# you need to change this to objdetect_mp4
# Note that on Android, mp4/gif does not work. iOS only.
# https://portal/zm/index.php?view=image&eid=EVENTID&fid=objdetect&width=600
# https://portal/zm/index.php?view=image&eid=EVENTID&fid=snapshot&width=600
ES_ZM_USER=
ES_ZM_PASSWORD=
ES_ZM_PORTAL=
ES_ZM_API_PORTAL=
ES_ZMES_PICTURE_URL=
ES_SSL=no
ES_CERT_FILE=/fake/path
ES_KEY_FILE=/fake/path
ES_MQTT_ENABLED=no
ES_MQTT_HOST=
ES_MQTT_USERNAME=
ES_MQTT_PASSWORD=
ES_USE_INTERFACE=no
ES_CONTROL_INTERFACE_PASSWORD=
ML_GATEWAY=
ML_USER=
ML_PASSWORD=
ML_PLATEREC_ALPR_KEY=
ML_OPENALPR_ALPR_KEY=
Running the Docker image
Finally you want to setup docker-compose to bring up zoneminder and its dependencies (if you choose to use them)
You can generate secrets using openssl rand -base64 32 > secret_file but keep in mind that the zoneminder user should use -hex 15 instead as it is limited by length and special characters. Also remember to create the zoneminder user and enable ZM_AUTH and setup a hash (using a random string too).
version: '3.7'
networks:
default:
external:
name: your-network
secrets:
mysql_user_secret:
file: ./secrets/mysql_user
mysql_root_secret:
file: ./secrets/mysql_root
rabbitmq_mqtt_secret:
file: ./secrets/rabbitmq_mqtt
zoneminder_user:
file: ./secrets/zoneminder_user
zoneminder_secret:
file: ./secrets/zoneminder_password
services:
rabbitmq:
container_name: rabbitmq
image: rabbitmq
restart: always
secrets:
- rabbitmq_mqtt_secret
environment:
- MQTT_PWD_FILE=/run/secrets/rabbitmq_mqqt_secret
healthcheck:
test: ["CMD", "rabbitmq-diagnostics" ,"ping", "-q"]
interval: 20s
timeout: 20s
retries: 10
mysql:
container_name: mysql
restart: always
image: mysql
secrets:
- mysql_user_secret
- mysql_root_secret
environment:
- TZ=
- MYSQL_USER=mysql
- MYSQL_PASSWORD_FILE=/run/secrets/mysql_user_secret
- MYSQL_ROOT_PASSWORD_FILE=/run/secrets/mysql_root_secret
- MYSQL_ROOT_HOST=%
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
interval: 20s
timeout: 20s
retries: 10
zoneminder:
container_name: zoneminder
image: your/zoneminder
restart: always
shm_size: 8G
secrets:
- mysql_user_secret
- mysql_root_secret
- rabbitmq_mqtt_secret
- zoneminder_user
- zoneminder_secret
runtime: nvidia
environment:
- TZ=
- PUID=1000
- PGID=1000
- CONF_OVERRIDE=yes
- ZM_DB_HOST=mysql
- ZM_DB_NAME=zm
- ZM_DB_USER=mysql
- ZM_DB_PASS_FILE=/run/secrets/mysql_user_secret
- ZM_DB_ROOT_PASS_FILE=/run/secrets/mysql_root_secret
- ES_ZM_PORTAL=https://{your-domain}
- ES_ZM_API_PORTAL=https://{your-domain}/api
- ES_ZMES_PICTURE_URL=https://{your-domain}/index.php?view=image&eid=EVENTID&fid=objdetect&width=600
- ES_ZM_USER_FILE=/run/secrets/zoneminder_user
- ES_ZM_PASSWORD_FILE=/run/secrets/zoneminder_secret
- ES_ES_SSL=no
- ES_MQTT_ENABLED=yes
- ES_MQTT_HOST=rabbitmq
- ES_MQTT_USERNAME=rabbitmq_mqtt
- ES_MQTT_PASSWORD_FILE=/run/secrets/rabbitmq_mqtt_secret
depends_on:
- "mysql"
- "rabbitmq"
You will need to deploy an additional Docker container to expose Zoneminder via a reverse proxy. That will be covered in a separate Gist later on.
To setup rabbit mqtt you will need to execute the following in the container
MQTT_PWD=`cat /run/secrets/rabbitmq_mqtt_secret`
rabbitmq-plugins enable rabbitmq_mqtt
rabbitmqctl add_user rabbitmq_mqtt ${MQTT_PWD}
rabbitmqctl set_permissions -p / rabbitmq_mqtt ".*" ".*" ".*"
rabbitmqctl set_user_tags rabbitmq_mqtt management