├── .gitattributes ├── .gitignore ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── update-version.yaml │ └── docker-build.yaml ├── rootfs ├── etc │ ├── services.d │ │ ├── mariadb │ │ │ ├── notification-fd │ │ │ ├── log │ │ │ │ └── run │ │ │ ├── finish │ │ │ ├── data │ │ │ │ └── check │ │ │ └── run │ │ ├── socklog │ │ │ ├── notification-fd │ │ │ ├── run │ │ │ └── log │ │ │ │ └── run │ │ ├── zoneminder │ │ │ ├── notification-fd │ │ │ ├── timeout-finish │ │ │ ├── log │ │ │ │ └── run │ │ │ ├── finish │ │ │ └── run │ │ ├── mariadb-configure │ │ │ ├── notification-fd │ │ │ ├── log │ │ │ │ └── run │ │ │ └── run │ │ ├── nginx │ │ │ ├── log │ │ │ │ └── run │ │ │ └── run │ │ ├── php-fpm │ │ │ ├── log │ │ │ │ └── run │ │ │ └── run │ │ └── fcgiwrap │ │ │ ├── log │ │ │ └── run │ │ │ ├── finish │ │ │ └── run │ ├── msmtprc │ ├── cont-init.d │ │ ├── 10-log-config.sh │ │ ├── 50-nginx-config.sh │ │ ├── 40-msmtp-config.sh │ │ ├── 00-reconfigure-user.sh │ │ ├── 30-zm-config.sh │ │ └── 20-system-config.sh │ ├── nginx │ │ └── nginx.conf │ └── mariadbconfigure.d │ │ └── 10-zoneminder-config.sh └── usr │ └── local │ └── bin │ └── logger ├── test ├── README.md ├── docker-compose.yml └── docker-compose-multi.yml ├── .env ├── docker-compose.test.yml ├── docker-compose.yml ├── docker-compose-multi.yml ├── parse_control.py ├── README.md ├── publish.py └── Dockerfile /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | test/zm/* -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: alexyao2015 2 | -------------------------------------------------------------------------------- /rootfs/etc/services.d/mariadb/notification-fd: -------------------------------------------------------------------------------- 1 | 3 -------------------------------------------------------------------------------- /rootfs/etc/services.d/socklog/notification-fd: -------------------------------------------------------------------------------- 1 | 3 -------------------------------------------------------------------------------- /rootfs/etc/services.d/zoneminder/notification-fd: -------------------------------------------------------------------------------- 1 | 3 -------------------------------------------------------------------------------- /rootfs/etc/services.d/zoneminder/timeout-finish: -------------------------------------------------------------------------------- 1 | 30000 -------------------------------------------------------------------------------- /rootfs/etc/services.d/mariadb-configure/notification-fd: -------------------------------------------------------------------------------- 1 | 3 -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Warning! 2 | 3 | Do not use the configuration files in this directory for anything other than testing! -------------------------------------------------------------------------------- /rootfs/etc/services.d/nginx/log/run: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | program_name="nginx" 3 | 4 | s6-format-filter "%1 %s" "[${program_name}]" | logutil-service /log/"${program_name}" 5 | -------------------------------------------------------------------------------- /rootfs/etc/services.d/mariadb/log/run: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | program_name="mariadb" 3 | 4 | s6-format-filter "%1 %s" "[${program_name}]" | logutil-service /log/"${program_name}" 5 | -------------------------------------------------------------------------------- /rootfs/etc/services.d/php-fpm/log/run: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | program_name="php-fpm" 3 | 4 | s6-format-filter "%1 %s" "[${program_name}]" | logutil-service /log/"${program_name}" 5 | -------------------------------------------------------------------------------- /rootfs/etc/services.d/fcgiwrap/log/run: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | program_name="fcgiwrap" 3 | 4 | s6-format-filter "%1 %s" "[${program_name}]" | logutil-service /log/"${program_name}" 5 | -------------------------------------------------------------------------------- /rootfs/etc/services.d/zoneminder/log/run: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | program_name="zoneminder-service" 3 | 4 | s6-format-filter "%1 %s" "[${program_name}]" | logutil-service /log/"${program_name}" 5 | -------------------------------------------------------------------------------- /rootfs/etc/services.d/mariadb-configure/log/run: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | program_name="mariadb-configure" 3 | 4 | s6-format-filter "%1 %s" "[${program_name}]" | logutil-service /log/"${program_name}" 5 | -------------------------------------------------------------------------------- /rootfs/etc/msmtprc: -------------------------------------------------------------------------------- 1 | account default 2 | 3 | host EMAIL_HOST 4 | 5 | port EMAIL_PORT 6 | tls on 7 | tls_starttls on 8 | 9 | auth login 10 | user EMAIL_USER 11 | from EMAIL_ADDRESS 12 | password EMAIL_PASSWORD 13 | 14 | syslog LOG_MAIL 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: "docker" 8 | directory: "/" 9 | schedule: 10 | interval: weekly -------------------------------------------------------------------------------- /rootfs/etc/services.d/socklog/run: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | . "/usr/local/bin/logger" 3 | # ============================================================================== 4 | # Socklog 5 | # Starts socklog to capture logs 6 | # ============================================================================== 7 | 8 | s6-socklog -d3 -t3000 9 | -------------------------------------------------------------------------------- /rootfs/etc/services.d/mariadb/finish: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | # ============================================================================== 3 | # Mariadb 4 | # Take down the S6 supervision tree if db is no longer accessible 5 | # ============================================================================== 6 | 7 | /run/s6/basedir/bin/halt 8 | -------------------------------------------------------------------------------- /rootfs/etc/services.d/mariadb/data/check: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | 3 | for _ in {1..3}; do 4 | if ! (fdmove -c 2 1 \ 5 | mysql --connect-timeout=1 -u"${MYSQL_USER}" -p"${MYSQL_PASSWORD}" -h"${MYSQL_HOST}" -e 'USE zm;' \ 6 | > /dev/null); then 7 | exit 1 # mysql is not running 8 | fi 9 | sleep 1 10 | done 11 | 12 | exit # mysql is running 13 | -------------------------------------------------------------------------------- /rootfs/etc/cont-init.d/10-log-config.sh: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | . "/usr/local/bin/logger" 3 | program_name="log-config" 4 | 5 | echo "Configuring log rotation with a maximum of ${MAX_LOG_NUMBER} logs and a max log size of ${MAX_LOG_SIZE_BYTES} bytes" | info "[${program_name}] " 6 | echo -n "1 n${MAX_LOG_NUMBER} s${MAX_LOG_SIZE_BYTES}" > /run/s6/container_environment/S6_LOGGING_SCRIPT 7 | -------------------------------------------------------------------------------- /rootfs/etc/cont-init.d/50-nginx-config.sh: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | . "/usr/local/bin/logger" 3 | program_name="nginx-config" 4 | 5 | echo "Configuring nginx settings..." | info "[${program_name}] " 6 | sed -i "s/FASTCGI_BUFFERS_CONFIGURATION_STRING/${FASTCGI_BUFFERS_CONFIGURATION_STRING}/g" /etc/nginx/nginx.conf 7 | sed -i "s/PHP_VERSION_ENVIRONMENT_VARIABLE/${PHP_VERSION}/g" /etc/nginx/nginx.conf 8 | -------------------------------------------------------------------------------- /rootfs/etc/services.d/socklog/log/run: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | program_name="zoneminder" 3 | 4 | s6-tai64n | s6-tai64nlocal | s6-format-filter "%1 %s" "[${program_name}]" | \ 5 | s6-envuidgid -D 65534:65534 nobody \ 6 | s6-log -b -- \ 7 | - \ 8 | +local1.* \ 9 | n"${MAX_LOG_NUMBER}" \ 10 | s"${MAX_LOG_SIZE_BYTES}" \ 11 | 1 \ 12 | /log/"${program_name}" 13 | -------------------------------------------------------------------------------- /rootfs/etc/services.d/fcgiwrap/finish: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | . "/usr/local/bin/logger" 3 | program_name="fcgiwrap" 4 | # ============================================================================== 5 | # fcgiwrap 6 | # Terminate fcgiwrap 7 | # ============================================================================== 8 | 9 | echo "Stopping fcgiwrap" | info "${program_name} " 10 | killall -15 fcgiwrap 11 | -------------------------------------------------------------------------------- /rootfs/etc/services.d/zoneminder/finish: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | . "/usr/local/bin/logger" 3 | # ============================================================================== 4 | # ZoneMinder 5 | # Take down the S6 supervision tree if db is no longer accessible 6 | # ============================================================================== 7 | 8 | /run/s6/basedir/bin/halt 9 | 10 | s6-setuidgid www-data /usr/bin/zmpkg.pl stop 11 | -------------------------------------------------------------------------------- /rootfs/etc/services.d/php-fpm/run: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | . "/usr/local/bin/logger" 3 | # ============================================================================== 4 | # PHP-fpm 5 | # Runs PHP-fpm 6 | # ============================================================================== 7 | 8 | echo "Starting PHP-fpm..." | info 9 | 10 | exec fdmove -c 2 1 \ 11 | /usr/sbin/php-fpm"${PHP_VERSION}" --nodaemonize --fpm-config /etc/php/"${PHP_VERSION}"/fpm/php-fpm.conf 12 | -------------------------------------------------------------------------------- /rootfs/etc/services.d/nginx/run: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | . "/usr/local/bin/logger" 3 | # ============================================================================== 4 | # Nginx 5 | # Runs Nginx 6 | # ============================================================================== 7 | 8 | echo "Waiting for ZoneMinder to start" | info 9 | s6-svwait -U /run/service/zoneminder 10 | 11 | echo "Starting Nginx..." | info 12 | 13 | exec fdmove -c 2 1 \ 14 | /usr/sbin/nginx -g 'daemon off;' 15 | -------------------------------------------------------------------------------- /rootfs/etc/cont-init.d/40-msmtp-config.sh: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | . "/usr/local/bin/logger" 3 | program_name="msmtp-config" 4 | 5 | EMAIL_USER=${EMAIL_USER:-"${EMAIL_ADDRESS}"} 6 | 7 | echo "Configuring msmtp settings..." | info "[${program_name}] " 8 | sed -i "s/EMAIL_HOST/${EMAIL_HOST}/g" /etc/msmtprc 9 | sed -i "s/EMAIL_PORT/${EMAIL_PORT}/g" /etc/msmtprc 10 | sed -i "s/EMAIL_ADDRESS/${EMAIL_ADDRESS}/g" /etc/msmtprc 11 | sed -i "s/EMAIL_USER/${EMAIL_USER}/g" /etc/msmtprc 12 | sed -i "s/EMAIL_PASSWORD/${EMAIL_PASSWORD}/g" /etc/msmtprc 13 | -------------------------------------------------------------------------------- /rootfs/etc/services.d/fcgiwrap/run: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | . "/usr/local/bin/logger" 3 | # ============================================================================== 4 | # fcgiwrap 5 | # Runs fcgiwrap 6 | # ============================================================================== 7 | 8 | echo "Starting fcgiwrap..." | info 9 | 10 | # Remove old socket if existing 11 | rm /zoneminder/run/fcgiwrap.socket > /dev/null 2>&1 12 | 13 | exec s6-setuidgid www-data \ 14 | fdmove -c 2 1 \ 15 | /usr/sbin/fcgiwrap -f -c "${FCGIWRAP_PROCESSES}" -s unix:/zoneminder/run/fcgiwrap.socket 16 | -------------------------------------------------------------------------------- /rootfs/usr/local/bin/logger: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | log() { 4 | echo "${1}" | s6-tai64n | s6-tai64nlocal | cat 5 | } 6 | 7 | # init service logtext 8 | init() { 9 | while read -r data_in; do 10 | log "${1}FIRSTRUN ${data_in}" 11 | done 12 | } 13 | 14 | # info service logtext 15 | info() { 16 | while read -r data_in; do 17 | log "${1}INFO ${data_in}" 18 | done 19 | } 20 | 21 | # warn service logtext 22 | warn() { 23 | while read -r data_in; do 24 | log "${1}WARN ${data_in}" 25 | done 26 | } 27 | 28 | # warn service logtext 29 | error() { 30 | while read -r data_in; do 31 | log "${1}ERROR ${data_in}" 32 | done 33 | } 34 | -------------------------------------------------------------------------------- /rootfs/etc/services.d/mariadb/run: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | . "/usr/local/bin/logger" 3 | # ============================================================================== 4 | # Mariadb 5 | # Stub service to monitor db status 6 | # ============================================================================== 7 | 8 | s6-notifyoncheck -n 1000 echo "Waiting for Mariadb to start" | info 9 | 10 | # Wait until db is initially ready before polling 11 | s6-svwait -U /run/service/mariadb 12 | echo "Mariadb is up! Proceeding to monitoring." | info 13 | 14 | # Need to sleep to act like service is running 15 | # Terminate container if db dies 16 | until ! (/run/service/mariadb/data/check); do 17 | sleep 1 18 | done 19 | echo "Mariadb could not be reached! Exiting..." | error 20 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | MYSQL_ROOT_PASSWORD=rootpassword 2 | MYSQL_USER=mysqluser 3 | MYSQL_PASSWORD=mysqlpassword 4 | 5 | TZ=America/Chicago 6 | 7 | MAX_LOG_SIZE_BYTES=1000000 8 | MAX_LOG_NUMBER=20 9 | 10 | EMAIL_HOST=smtp.gmail.com 11 | EMAIL_PORT=587 12 | EMAIL_ADDRESS=coolemail@gmail.com 13 | EMAIL_PASSWORD=very_secure_password 14 | 15 | # Don't change these unless you know what you're doing 16 | # MYSQL_HOST=db 17 | # PHP_MAX_CHILDREN=120 18 | # PHP_START_SERVERS=12 19 | # PHP_MIN_SPARE_SERVERS=6 20 | # PHP_MAX_SPARE_SERVERS=18 21 | # PHP_MEMORY_LIMIT=2048M 22 | # PHP_MAX_EXECUTION_TIME=600 23 | # PHP_MAX_INPUT_VARIABLES=3000 24 | # PHP_MAX_INPUT_TIME=600 25 | # FCGIWRAP_PROCESSES=15 26 | # FASTCGI_BUFFERS_CONFIGURATION_STRING="64 4K" 27 | # PUID=911 28 | # PGID=911 29 | # USE_SECURE_RANDOM_ORG=1 30 | # EMAIL_USER=cooluser 31 | -------------------------------------------------------------------------------- /rootfs/etc/services.d/mariadb-configure/run: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | . "/usr/local/bin/logger" 3 | # ============================================================================== 4 | # mariadb-configure 5 | # Configures db using /etc/mariadbconfigure.d 6 | # ============================================================================== 7 | 8 | # Reconfigure to be oneshot 9 | s6-svc -O /run/service/mariadb-configure 10 | 11 | echo "Waiting for MariaDB to start" | info 12 | # Wait for db to be up before configuring 13 | s6-svwait -U /run/service/mariadb 14 | 15 | for f in /etc/mariadbconfigure.d/*.sh; do 16 | bash "$f" 17 | done 18 | 19 | echo "Upgrading db if necessary" | info 20 | s6-setuidgid www-data /usr/bin/zmupdate.pl -nointeractive | info 21 | 22 | echo "Refreshing db" | info 23 | s6-setuidgid www-data /usr/bin/zmupdate.pl -nointeractive -f | info 24 | 25 | # Notify s6 service is up 26 | fdmove 1 3 printf "done\n" 27 | -------------------------------------------------------------------------------- /test/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | 3 | services: 4 | db: 5 | image: mariadb 6 | restart: always 7 | networks: 8 | - zoneminder 9 | volumes: 10 | - ./zm/db:/var/lib/mysql 11 | environment: 12 | - MYSQL_DATABASE=zm 13 | env_file: 14 | - ../.env 15 | 16 | zoneminder: 17 | build: 18 | context: .. 19 | dockerfile: ./Dockerfile 20 | restart: always 21 | stop_grace_period: 45s 22 | ports: 23 | - 80:80 24 | networks: 25 | - zoneminder 26 | volumes: 27 | - ./zm/data:/data 28 | - ./zm/config:/config 29 | - ./zm/log:/log 30 | - type: tmpfs 31 | target: /dev/shm 32 | tmpfs: 33 | size: 1000000000 34 | env_file: 35 | - ../.env 36 | # command: ["/bin/sh", "-ec", "while :; do sleep 5 ; done"] 37 | # environment: 38 | # - ZM_SERVER_HOST=zoneminder1 39 | 40 | networks: 41 | zoneminder: 42 | -------------------------------------------------------------------------------- /docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | 3 | services: 4 | db: 5 | container_name: db 6 | image: mariadb 7 | restart: always 8 | networks: 9 | - zoneminder 10 | volumes: 11 | - ./zm/db:/var/lib/mysql 12 | environment: 13 | - MYSQL_DATABASE=zm 14 | env_file: 15 | - ./.env 16 | 17 | zoneminder: 18 | container_name: zoneminder 19 | image: ci:latest 20 | # Disable automatic restarts so we know if container dies on startup 21 | deploy: 22 | restart_policy: 23 | condition: none 24 | # Make grace period extremely long so test fails if 25 | # container doesn't automatically terminate 26 | stop_grace_period: 1h 27 | depends_on: 28 | - db 29 | ports: 30 | - 80:80 31 | networks: 32 | - zoneminder 33 | volumes: 34 | - ./zm/data:/data 35 | - ./zm/config:/config 36 | - ./zm/log:/log 37 | env_file: 38 | - ./.env 39 | environment: 40 | - ZM_SERVER_HOST=zoneminder1 41 | 42 | networks: 43 | zoneminder: 44 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | 3 | services: 4 | # !! WARNING: !! 5 | # If using linuxserver/mariadb, you will need to add 6 | # skip-log-bin to mysqld and disable all log_bin options 7 | db: 8 | image: mariadb 9 | restart: always 10 | networks: 11 | - zoneminder 12 | volumes: 13 | - ./zm/db:/var/lib/mysql 14 | environment: 15 | - MYSQL_DATABASE=zm 16 | env_file: 17 | - ./.env 18 | 19 | zoneminder: 20 | image: ghcr.io/zoneminder-containers/zoneminder-base:latest 21 | restart: always 22 | stop_grace_period: 45s 23 | depends_on: 24 | - db 25 | ports: 26 | - 80:80 27 | networks: 28 | - zoneminder 29 | volumes: 30 | - ./zm/data:/data 31 | - ./zm/config:/config 32 | - ./zm/log:/log 33 | - type: tmpfs 34 | target: /dev/shm 35 | tmpfs: 36 | size: 1000000000 37 | env_file: 38 | - ./.env 39 | # environment: 40 | # - ZM_SERVER_HOST=zoneminder1 41 | 42 | 43 | networks: 44 | zoneminder: 45 | -------------------------------------------------------------------------------- /rootfs/etc/services.d/zoneminder/run: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | . "/usr/local/bin/logger" 3 | # ============================================================================== 4 | # ZoneMinder 5 | # Runs ZoneMinder 6 | # ============================================================================== 7 | 8 | echo "Waiting for Socklog to start" | info 9 | s6-svwait -U /run/service/socklog 10 | echo "Waiting for MariaDB to start" | info 11 | s6-svwait -U /run/service/mariadb 12 | echo "Waiting for mariadb-configure to complete..." | info 13 | s6-svwait -U /run/service/mariadb-configure 14 | 15 | echo "Starting ZoneMinder..." | info 16 | s6-setuidgid www-data /usr/bin/zmpkg.pl start 17 | 18 | # zmpkg.pl is blocking so no need to wait 19 | echo "ZoneMinder is up! Proceeding to monitoring." | info 20 | # Notify s6 service is up 21 | fdmove 1 3 printf "done\n" 22 | 23 | # Need to sleep to act like service is running 24 | # Terminate container if zm dies 25 | until [ "$(pgrep -fc /usr/bin/zm)" -lt "1" ]; do 26 | sleep 1 27 | done 28 | echo "ZoneMinder has crashed! Exiting..." | error 29 | -------------------------------------------------------------------------------- /rootfs/etc/cont-init.d/00-reconfigure-user.sh: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | . "/usr/local/bin/logger" 3 | program_name="reconfigure-user" 4 | 5 | PUID=${PUID:-911} 6 | PGID=${PGID:-911} 7 | 8 | if [ "${PUID}" -ne 911 ] || [ "${PGID}" -ne 911 ]; then 9 | echo "Reconfiguring GID and UID" | info "[${program_name}] " 10 | groupmod -o -g "$PGID" www-data 11 | usermod -o -u "$PUID" www-data 12 | 13 | echo "User uid: $(id -u www-data)" | info "[${program_name}] " 14 | echo "User gid: $(id -g www-data)" | info "[${program_name}] " 15 | 16 | echo "Setting permissions for user www-data" | info "[${program_name}] " 17 | chown -R www-data:www-data \ 18 | /config \ 19 | /zoneminder 20 | chmod -R 755 \ 21 | /config \ 22 | /zoneminder 23 | else 24 | echo "Setting permissions for user www-data" | info "[${program_name}] " 25 | chown -R www-data:www-data \ 26 | /config 27 | chmod -R 755 \ 28 | /config 29 | fi 30 | 31 | echo "Setting permissions for user nobody at /log" | info "[${program_name}] " 32 | chown -R nobody:nogroup \ 33 | /log 34 | chmod -R 755 \ 35 | /log 36 | -------------------------------------------------------------------------------- /docker-compose-multi.yml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | 3 | services: 4 | db: 5 | image: mariadb 6 | restart: always 7 | networks: 8 | - zoneminder 9 | volumes: 10 | - ./zm/db:/var/lib/mysql 11 | environment: 12 | - MYSQL_DATABASE=zm 13 | env_file: 14 | - ./.env 15 | 16 | zoneminder1: 17 | image: ghcr.io/zoneminder-containers/zoneminder-base:latest 18 | restart: always 19 | stop_grace_period: 45s 20 | depends_on: 21 | - db 22 | ports: 23 | - 80:80 24 | networks: 25 | - zoneminder 26 | volumes: 27 | - ./zm/data:/data 28 | - ./zm/config1:/config 29 | - ./zm/log1:/log 30 | - type: tmpfs 31 | target: /dev/shm 32 | tmpfs: 33 | size: 1000000000 34 | env_file: 35 | - ./.env 36 | environment: 37 | - ZM_SERVER_HOST=zoneminder1 38 | 39 | zoneminder2: 40 | image: ghcr.io/zoneminder-containers/zoneminder-base:latest 41 | restart: always 42 | stop_grace_period: 45s 43 | depends_on: 44 | - db 45 | ports: 46 | - 81:80 47 | networks: 48 | - zoneminder 49 | shm_size: 1G 50 | volumes: 51 | - ./zm/data:/data 52 | - ./zm/config2:/config 53 | - ./zm/log2:/log 54 | env_file: 55 | - ./.env 56 | environment: 57 | - ZM_SERVER_HOST=zoneminder2 58 | 59 | networks: 60 | zoneminder: 61 | -------------------------------------------------------------------------------- /test/docker-compose-multi.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | db: 5 | image: mariadb 6 | restart: always 7 | networks: 8 | - zoneminder 9 | volumes: 10 | - ./zm/db:/var/lib/mysql 11 | environment: 12 | - MYSQL_DATABASE=zm 13 | env_file: 14 | - ../.env 15 | 16 | zoneminder1: 17 | build: 18 | context: .. 19 | dockerfile: ./Dockerfile 20 | restart: always 21 | stop_grace_period: 45s 22 | depends_on: 23 | - db 24 | ports: 25 | - 80:80 26 | networks: 27 | - zoneminder 28 | volumes: 29 | - ./zm/data:/data 30 | - ./zm/config1:/config 31 | - ./zm/log1:/log 32 | - type: tmpfs 33 | target: /dev/shm 34 | tmpfs: 35 | size: 1000000000 36 | env_file: 37 | - ../.env 38 | environment: 39 | - ZM_SERVER_HOST=zoneminder1 40 | 41 | zoneminder2: 42 | build: 43 | context: .. 44 | dockerfile: ./Dockerfile 45 | restart: always 46 | stop_grace_period: 45s 47 | depends_on: 48 | - db 49 | ports: 50 | - 81:80 51 | networks: 52 | - zoneminder 53 | shm_size: 1G 54 | volumes: 55 | - ./zm/data:/data 56 | - ./zm/config2:/config 57 | - ./zm/log2:/log 58 | env_file: 59 | - ../.env 60 | environment: 61 | - ZM_SERVER_HOST=zoneminder2 62 | 63 | networks: 64 | zoneminder: 65 | -------------------------------------------------------------------------------- /rootfs/etc/cont-init.d/30-zm-config.sh: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | . "/usr/local/bin/logger" 3 | program_name="zm-config" 4 | 5 | # Configure zm config folder 6 | if [ ! -f "/config/zm.conf" ]; then 7 | echo "Configuring ZoneMinder Configuration folder" | init "[${program_name}] " 8 | s6-setuidgid www-data \ 9 | cp -r /zoneminder/defaultconfig/* /config 10 | fi 11 | 12 | # Setup required data folders 13 | if [ ! -d "/data/events" ]; then 14 | echo "Configuring ZoneMinder Data folder" | init "[${program_name}] " 15 | # Only configure data permissions once to prevent massive startup lag 16 | chmod -R 755 /data 17 | chown www-data:www-data /data 18 | s6-setuidgid www-data \ 19 | mkdir -p \ 20 | /data/events \ 21 | /data/images 22 | fi 23 | 24 | ## Configure ZoneMinder DB 25 | echo "Configuring ZoneMinder db Settings" | info "[${program_name}] " 26 | sed -i "s/ZM_DB_USER=.*$/ZM_DB_USER=${MYSQL_USER}/g" /config/zm.conf 27 | sed -i "s/ZM_DB_PASS=.*$/ZM_DB_PASS=${MYSQL_PASSWORD}/g" /config/zm.conf 28 | sed -i "s/ZM_DB_HOST=.*$/ZM_DB_HOST=${MYSQL_HOST}/g" /config/zm.conf 29 | # This cannot be changed 30 | sed -i "s/ZM_DB_NAME=.*$/ZM_DB_NAME=zm/g" /config/zm.conf 31 | 32 | if [[ -n "${ZM_SERVER_HOST}" ]]; then 33 | echo "Configuring ZoneMinder ZM_SERVER_HOST for Multi-Server Support" | info "[${program_name}] " 34 | sed -i "s/ZM_SERVER_HOST=.*$/ZM_SERVER_HOST=${ZM_SERVER_HOST}/g" /config/zm.conf 35 | fi 36 | -------------------------------------------------------------------------------- /rootfs/etc/cont-init.d/20-system-config.sh: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | . "/usr/local/bin/logger" 3 | program_name="system-config" 4 | 5 | ## Configure Timezone 6 | echo "Setting system timezone to ${TZ}" | info "[${program_name}] " 7 | ln -sf "/usr/share/zoneinfo/$TZ" /etc/localtime 8 | 9 | ## Set PHP Time 10 | 11 | echo "Configuring PHP Time" | info "[${program_name}] " 12 | # PHP_INSTALL=`php -r "echo php_ini_loaded_file().PHP_EOL;"` 13 | PHP_VERSION=$(php -r "echo PHP_MAJOR_VERSION;" && echo -n "." && php -r "echo PHP_MINOR_VERSION;") 14 | echo -n "${PHP_VERSION}" > /run/s6/container_environment/PHP_VERSION 15 | 16 | echo "date.timezone = ${TZ}" >> /etc/php/"${PHP_VERSION}"/fpm/conf.d/30-zoneminder-time.ini 17 | 18 | echo "Applying PHP Optimizations" | info "[${program_name}] " 19 | sed -i "s/pm.max_children =.*/pm.max_children = ${PHP_MAX_CHILDREN}/" /etc/php/"${PHP_VERSION}"/fpm/pool.d/www.conf 20 | sed -i "s/pm.start_servers =.*/pm.start_servers = ${PHP_START_SERVERS}/" /etc/php/"${PHP_VERSION}"/fpm/pool.d/www.conf 21 | sed -i "s/pm.min_spare_servers =.*/pm.min_spare_servers = ${PHP_MIN_SPARE_SERVERS}/" /etc/php/"${PHP_VERSION}"/fpm/pool.d/www.conf 22 | sed -i "s/pm.max_spare_servers =.*/pm.max_spare_servers = ${PHP_MAX_SPARE_SERVERS}/" /etc/php/"${PHP_VERSION}"/fpm/pool.d/www.conf 23 | 24 | echo "memory_limit = ${PHP_MEMORY_LIMIT} 25 | max_execution_time = ${PHP_MAX_EXECUTION_TIME} 26 | max_input_vars = ${PHP_MAX_INPUT_VARIABLES} 27 | max_input_time = ${PHP_MAX_INPUT_TIME}" > /etc/php/"${PHP_VERSION}"/fpm/conf.d/30-zoneminder.ini 28 | 29 | echo "Redirecting PHP Logs to stdout" | info "[${program_name}] " 30 | ln -sf /proc/self/fd/1 /var/log/php"${PHP_VERSION}"-fpm.log 31 | -------------------------------------------------------------------------------- /.github/workflows/update-version.yaml: -------------------------------------------------------------------------------- 1 | name: Update ZoneMinder Version 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | name: Update ZoneMinder Version 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v6 14 | - name: Update 15 | id: update 16 | run: | 17 | set -x 18 | wget \ 19 | --no-check-certificate -qO - \ 20 | https://api.github.com/repos/ZoneMinder/zoneminder/releases/latest \ 21 | | awk '/tag_name/{print $4;exit}' FS='[""]' \ 22 | > latest_version.txt 23 | export VERSION=`cat latest_version.txt` 24 | echo "version=${VERSION}" >> $GITHUB_OUTPUT 25 | - name: Push tag 26 | id: tag_version 27 | if: ${{ steps.update.outputs.version != '' }} 28 | uses: mathieudutour/github-tag-action@v6.2 29 | with: 30 | custom_tag: ${{ steps.update.outputs.version }} 31 | tag_prefix: '' 32 | # Custom PAT required to trigger build workflow 33 | github_token: ${{ secrets.GHCR_PAT }} 34 | # - name: Create Pull Request 35 | # uses: peter-evans/create-pull-request@v3 36 | # with: 37 | # committer: GitHub 38 | # author: GitHub 39 | # commit-message: Update ZoneMinder version to ${{ steps.update.outputs.version }} 40 | # branch: action/update-zmversion 41 | # delete-branch: true 42 | # title: Bump version to ${{ steps.update.outputs.version }} 43 | # body: Bumps version to ${{ steps.update.outputs.version }}. See [ZoneMinder Releases](https://github.com/ZoneMinder/zoneminder/releases). 44 | # labels: automated 45 | -------------------------------------------------------------------------------- /rootfs/etc/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | user www-data www-data; 2 | worker_processes auto; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | 8 | events { 9 | worker_connections 1024; 10 | } 11 | 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | 17 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 18 | '$status $body_bytes_sent "$http_referer" ' 19 | '"$http_user_agent" "$http_x_forwarded_for"'; 20 | 21 | access_log /var/log/nginx/access.log main; 22 | 23 | upstream php-handler { 24 | server unix:/run/php/phpPHP_VERSION_ENVIRONMENT_VARIABLE-fpm.sock; 25 | } 26 | 27 | server { 28 | listen 80; 29 | root /var/www/html; 30 | gzip off; 31 | include /etc/nginx/conf.d/*.conf; 32 | 33 | location / { 34 | index index.php; 35 | } 36 | location ^~ /zm { 37 | rewrite ^/zm(.*)$ $1 last; 38 | } 39 | location /cache { 40 | alias /zoneminder/cache; 41 | } 42 | 43 | location /api/ { 44 | rewrite ^/api(.+)$ /api/index.php?p=$1 last; 45 | } 46 | 47 | location /cgi-bin/ { 48 | root /zoneminder; 49 | fastcgi_pass unix:/zoneminder/run/fcgiwrap.socket; 50 | include /etc/nginx/fastcgi_params; 51 | fastcgi_param SCRIPT_FILENAME $request_filename; 52 | fastcgi_buffers FASTCGI_BUFFERS_CONFIGURATION_STRING; 53 | } 54 | 55 | location ~ \.php$ { 56 | if (!-f $request_filename) { return 404; } 57 | expires epoch; 58 | include /etc/nginx/fastcgi_params; 59 | fastcgi_param SCRIPT_FILENAME $request_filename; 60 | fastcgi_index index.php; 61 | fastcgi_pass php-handler; 62 | fastcgi_buffers FASTCGI_BUFFERS_CONFIGURATION_STRING; 63 | } 64 | 65 | location ~ \.(jpg|jpeg|gif|png|ico)$ { 66 | expires 33d; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /rootfs/etc/mariadbconfigure.d/10-zoneminder-config.sh: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | . "/usr/local/bin/logger" 3 | # ============================================================================== 4 | # ZoneMinder-config 5 | # Configure default ZM Settings 6 | # ============================================================================== 7 | 8 | insert_command="" 9 | 10 | if ! (fdmove -c 2 1 \ 11 | mysql -u"${MYSQL_USER}" -p"${MYSQL_PASSWORD}" -h"${MYSQL_HOST}" -e 'USE zm; SELECT * FROM Config LIMIT 1;' \ 12 | > /dev/null); then 13 | echo "Creating ZoneMinder db for first run" | init 14 | mysql -u"${MYSQL_USER}" -p"${MYSQL_PASSWORD}" -h"${MYSQL_HOST}" < /usr/share/zoneminder/db/zm_create.sql 15 | 16 | echo "Configuring ZoneMinder Email settings..." | init 17 | insert_command+="UPDATE Config SET Value = 1 WHERE Name = 'ZM_NEW_MAIL_MODULES';" 18 | insert_command+="UPDATE Config SET Value = 1 WHERE Name = 'ZM_OPT_EMAIL';" 19 | insert_command+="UPDATE Config SET Value = 1 WHERE Name = 'ZM_OPT_MESSAGE';" 20 | insert_command+="UPDATE Config SET Value = 1 WHERE Name = 'ZM_SSMTP_MAIL';" 21 | insert_command+="UPDATE Config SET Value = '/usr/bin/msmtp' WHERE Name = 'ZM_SSMTP_PATH';" 22 | insert_command+="UPDATE Config SET Value = '${EMAIL_ADDRESS}' WHERE Name = 'ZM_EMAIL_ADDRESS';" 23 | insert_command+="UPDATE Config SET Value = '${EMAIL_ADDRESS}' WHERE Name = 'ZM_FROM_EMAIL';" 24 | 25 | else 26 | 27 | echo "Configuring ZoneMinder Email From Address..." | info 28 | insert_command+="UPDATE Config SET Value = '${EMAIL_ADDRESS}' WHERE Name = 'ZM_FROM_EMAIL';" 29 | 30 | fi 31 | 32 | # Enforce disabling of file logs 33 | echo "Disabling file log to prevent duplicate logs from syslog" | info 34 | insert_command+="UPDATE Config SET Value = -5 WHERE Name = 'ZM_LOG_LEVEL_FILE';" 35 | 36 | if [[ -n "${ZM_SERVER_HOST}" ]] \ 37 | && [ "$(mysql -u"${MYSQL_USER}" -p"${MYSQL_PASSWORD}" -h"${MYSQL_HOST}" zm -e \ 38 | "SELECT COUNT(*) FROM Servers WHERE Name = '${ZM_SERVER_HOST}';" \ 39 | | cut -f 2 \ 40 | | sed -n '2 p' \ 41 | )" \ 42 | == "0" ]; then 43 | echo "Adding multi-server db entry" | init 44 | insert_command+="INSERT INTO \`Servers\` " 45 | insert_command+="(Protocol, Hostname, Port, PathToIndex, PathToZMS, PathToApi, Name, zmstats, zmaudit, zmtrigger, zmeventnotification)"; 46 | insert_command+=" VALUES " 47 | insert_command+="('http','${ZM_SERVER_HOST}',80,'/index.php','/cgi-bin/nph-zms','/zm/api','${ZM_SERVER_HOST}',1,1,1,0);"; 48 | fi 49 | 50 | echo "Applying db changes..." | info 51 | mysql -u"${MYSQL_USER}" -p"${MYSQL_PASSWORD}" -h"${MYSQL_HOST}" zm -e "${insert_command}" 52 | -------------------------------------------------------------------------------- /parse_control.py: -------------------------------------------------------------------------------- 1 | import re 2 | from shutil import copyfile 3 | from os import path 4 | from typing import Optional, List, Pattern, Tuple 5 | 6 | 7 | class PackageNotFound(Exception): 8 | pass 9 | 10 | 11 | def locate_distro() -> str: 12 | """Return base path for control/compat files.""" 13 | if path.isfile("distros/ubuntu2004/control"): 14 | control_location = "distros/ubuntu2004" 15 | else: 16 | control_location = "distros/ubuntu1604" 17 | return control_location 18 | 19 | 20 | def load_control() -> List[Optional[str]]: 21 | control_programs = [] 22 | base_location = locate_distro() 23 | control_location = path.join(base_location, "control") 24 | with open(control_location, "r") as file_obj: 25 | program = "" 26 | pkg_found = False 27 | for line in file_obj: 28 | if line.startswith("#"): 29 | continue 30 | if "Package: " in line: 31 | if pkg_found: 32 | control_programs.append(program) 33 | program = "" 34 | else: 35 | pkg_found = True 36 | program += line 37 | # append last program 38 | control_programs.append(program) 39 | return control_programs 40 | 41 | 42 | def remove_duplicate(input_str: str, duplicated_pattern: Pattern, replace: str = "") -> str: 43 | """Remove a duplicated pattern in a string.""" 44 | while re.search(duplicated_pattern, input_str): 45 | input_str = re.sub(duplicated_pattern, replace, input_str) 46 | return input_str 47 | 48 | 49 | def get_value(pkg_info: str, pkg_key: str) -> Optional[str]: 50 | """Return value of key. Cannot have duplicate value in string passed in.""" 51 | lines = pkg_info.split('\n') 52 | key_start = None # type: Optional[int] 53 | key_end = None # type: Optional[int] 54 | for line_num in range(len(lines)): 55 | if (line := lines[line_num]).startswith(pkg_key): 56 | key_start = line_num 57 | elif key_start and re.search(r"(^(?! ).*?)(:)", line): 58 | # Do not want to include this line but use it as a stopper 59 | key_end = line_num - 1 60 | break 61 | if key_start and key_end: 62 | key_value = "" 63 | for line_num in range(key_start, key_end + 1): 64 | key_value += lines[line_num] + "\n" 65 | return key_value 66 | else: 67 | return None 68 | 69 | 70 | def get_package(control_programs: List[str], package_name: str) -> str: 71 | """Get named package from control program list.""" 72 | for control_program in control_programs: 73 | item = re.sub(r'(^Package: )', '', get_value(control_program, "Package").rstrip()) 74 | if item == package_name: 75 | return control_program 76 | raise PackageNotFound 77 | 78 | 79 | control_pkg = get_package(load_control(), "zoneminder") 80 | 81 | with open("zoneminder_control", "w") as file_obj: 82 | file_obj.write(control_pkg) 83 | 84 | # Used as default for equivs-build template due to bug 85 | copyfile(path.join(locate_distro(), "compat"), "zoneminder_compat") 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zoneminder Container 2 | 3 | [![Docker Build](https://github.com/zoneminder-containers/zoneminder-base/actions/workflows/docker-build.yaml/badge.svg)](https://github.com/zoneminder-containers/zoneminder-base/actions/workflows/docker-build.yaml) 4 | [![DockerHub Pulls](https://img.shields.io/docker/pulls/yaoa/zoneminder-base.svg)](https://hub.docker.com/r/yaoa/zoneminder-base) 5 | ![Status](https://img.shields.io/badge/Status-Completed-brightgreen) 6 | 7 | # Maintenance Mode Notice 8 | I do not personally use this and as such will have limited time to invest into this project. Basic things will be fixed, 9 | however larger issues from either changes from Zoneminder or otherwise will likely not be fixed. If there is substantial 10 | demand for a new feature/improvement, I will consider implementing it if I have time. Sponsoring me and noting this is 11 | the reason you are doing so will help me invest more time into this as well. 12 | 13 | # Why 14 | This is an automatically updating ZoneMinder container built using s6-overlay with full support for all things containers. 15 | This aims to be the container that will never die as things will automatically keep themselves up to date and allow for 16 | easy selection/testing of various ZoneMinder versions. 17 | 18 | This container aims to follow all of the best practices of being a container meaning that the software and persistent 19 | data are separated, with the container remaining static. This means the container can easily be updated/restored provided 20 | the persistent data volumes are backed up. 21 | 22 | Not only does this aim to follow all of the best practices, but this also aims to be 23 | the easiest container with nearly everything configurable through environment variables 24 | or automatically/preconfigured for you! 25 | 26 | There is also full support for multi-server setups with automation to link all servers! 27 | 28 | # How 29 | 30 | 1. Install [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/) 31 | 2. Download docker-compose.yml or docker-compose-multi.yml depending on single/multi server setups. 32 | 3. Download .env 33 | 4. Place all these files in the same folder and configure .env and the yml files as you please. 34 | 5. Run `docker compose up -d` to start. 35 | 36 | NOTE: The default docker-compose.yml files use the `latest` tag which runs the latest release build of ZoneMinder. 37 | 38 | ## Defining a Version 39 | 40 | 1. Replace `latest` in the `docker-compose.yml` file with any ZoneMinder version you would like to run. 41 | You can find all available releases [here](https://github.com/zoneminder-containers/zoneminder-base/releases). 42 | Ex. `1.36.1` 43 | 44 | Note: For those new to Docker, these values are known as the container tag. 45 | 46 | ### Available Tags 47 | 48 | - `branch_name`: This is the branch name of this repository, not Zoneminder. 49 | - `release`: References the latest release 50 | - `latest`: Same as release 51 | - `nightly`: Nightly builds from master on Zoneminder 52 | 53 | ## Updates 54 | 55 | 1. Replace the tag with the new version to update to, or for `latest`, simply continue to the next step. 56 | 2. `docker compose pull` 57 | 3. `docker compose up -d` 58 | 59 | 60 | # Helpful Info 61 | Logs are rotated according to the [TAI64N standard](http://skarnet.org/software/s6/s6-log.html) 62 | 63 | `/data` is not included in fix-permissions because it takes a substantial amount of time to run for the events folder 64 | when there are a large number of files 65 | 66 | The web interface is accessible at the root directory. Do not use the /zm subdirectory to access the interface. 67 | 68 | # Issues: 69 | - Tell me? 70 | 71 | # Future Containers: 72 | 73 | 1. [eventserver-base](https://github.com/zoneminder-containers/eventserver-base) (Currently WIP) 74 | - Install ZM Event Server 75 | - Automatically enable Event Server and modify Servers table entry to enable Event Server 76 | 2. eventserver-mlapi-base 77 | - Install YOLO ML Models without opencv 78 | 3. eventserver-mlapi 79 | - Build and install standard opencv 80 | 4. eventserver-mlapi-cuda 81 | - Develop autobuilding opencv with cuda support container 82 | -------------------------------------------------------------------------------- /publish.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Script to publish docker images and manifests.""" 3 | from __future__ import annotations 4 | 5 | import argparse 6 | import shlex 7 | import subprocess 8 | import sys 9 | from dataclasses import dataclass 10 | from os import getenv 11 | 12 | ARCH_386 = "i386" 13 | ARCH_AMD64 = "amd64" 14 | ARCH_ARMV6 = "armv6" 15 | ARCH_ARMV7 = "armv7" 16 | ARCH_ARM64 = "arm64" 17 | ARCHS = [ARCH_386, ARCH_AMD64, ARCH_ARMV7, ARCH_ARM64] 18 | 19 | PUBLISH_LATEST = "{repo}:{arch}-latest" 20 | 21 | PUBLISH_NAMES = [ 22 | "{repo}:{arch}{run_number}", 23 | "{repo}:{arch}-{base_tag}", 24 | "{repo}:{arch}-{base_tag}{run_number}", 25 | "{repo}:{arch}-{base_tag}{github_sha}", 26 | ] 27 | 28 | MANIFEST_NAMES = [ 29 | "{repo}:{run_number}", 30 | "{repo}:{base_tag}", 31 | "{repo}:{base_tag}{run_number}", 32 | "{repo}:{base_tag}{github_sha}", 33 | ] 34 | 35 | parser = argparse.ArgumentParser() 36 | parser.add_argument("--tag", type=str, required=True, help="Base tag to publish") 37 | parser.add_argument( 38 | "--repo", 39 | type=str, 40 | required=True, 41 | help="Repo to publish to (includes registry if not Docker Hub)", 42 | ) 43 | parser.add_argument( 44 | "--latest", required=False, action="store_true", help="Publish latest tag" 45 | ) 46 | parser.add_argument( 47 | "--dry-run", 48 | required=False, 49 | action="store_true", 50 | help="Print commands only", 51 | ) 52 | parser.add_argument("--ci", required=False, action="store_true", help="Running in CI") 53 | parser.add_argument( 54 | "--run-number", 55 | type=str, 56 | required=False, 57 | default="", 58 | help="Run number to append to tags if ci is not specified", 59 | ) 60 | parser.add_argument( 61 | "--github-sha", 62 | type=str, 63 | required=False, 64 | default="", 65 | help="GitHub SHA to append to tags if ci is not specified", 66 | ) 67 | group = parser.add_mutually_exclusive_group(required=True) 68 | group.add_argument( 69 | "--image", action="store_true", help="Publish architecture image to all tags" 70 | ) 71 | group.add_argument( 72 | "--manifest", 73 | action="store_true", 74 | help="Publish manifest to all tags", 75 | ) 76 | opts, _ = parser.parse_known_args() 77 | if opts.image: 78 | parser.add_argument( 79 | "--arch", 80 | type=str, 81 | required=True, 82 | help="Architecture to publish", 83 | choices=ARCHS, 84 | ) 85 | parser.add_argument( 86 | "--image-name", 87 | type=str, 88 | required=True, 89 | help="Name of image to publish", 90 | ) 91 | 92 | 93 | @dataclass(frozen=True) 94 | class DockerNames: 95 | """Preformatted docker image names.""" 96 | 97 | ci_name: str | None 98 | names: list 99 | 100 | manifests: list 101 | manifest_base: str | None 102 | manifest_cmd_base: list | None 103 | 104 | # Remove duplicates caused by empty run_number or github_sha 105 | # Cleans docker command 106 | @staticmethod 107 | def _clean_list(duplicates: list) -> list: 108 | cleaned = [x.replace(":-", ":") for x in duplicates if not x.endswith(":")] 109 | cleaned = list(set(cleaned)) 110 | return cleaned 111 | 112 | @classmethod 113 | def from_args(cls, args): 114 | """Create names from args.""" 115 | if args.ci: 116 | run_number = getenv("GITHUB_RUN_NUMBER") 117 | github_sha = getenv("GITHUB_SHA") 118 | else: 119 | if args.run_number: 120 | run_number = args.run_number 121 | else: 122 | run_number = "" 123 | if args.github_sha: 124 | github_sha = args.github_sha 125 | else: 126 | github_sha = "" 127 | run_number = f"-{run_number}" 128 | github_sha = f"-{github_sha}" 129 | 130 | if not args.image: 131 | preformatted_names = [] 132 | ci_name = None 133 | else: 134 | preformatted_names = [ 135 | publish_name.format( 136 | repo=args.repo, 137 | arch=args.arch, 138 | base_tag=args.tag, 139 | run_number=run_number, 140 | github_sha=github_sha, 141 | ) 142 | for publish_name in PUBLISH_NAMES 143 | ] 144 | if args.latest: 145 | preformatted_names.append( 146 | PUBLISH_LATEST.format(repo=args.repo, arch=args.arch) 147 | ) 148 | preformatted_names = cls._clean_list(preformatted_names) 149 | ci_name = args.image_name 150 | 151 | if not args.manifest: 152 | preformatted_manifests = [] 153 | manifest_base = None 154 | manifest_cmd_base = None 155 | else: 156 | preformatted_manifests = [ 157 | manifest_name.format( 158 | repo=args.repo, 159 | arch="{arch}", 160 | base_tag=args.tag, 161 | run_number=run_number, 162 | github_sha=github_sha, 163 | ) 164 | for manifest_name in MANIFEST_NAMES 165 | ] 166 | if args.latest: 167 | preformatted_manifests.append(f"{args.repo}:latest") 168 | preformatted_manifests = cls._clean_list(preformatted_manifests) 169 | manifest_base = PUBLISH_NAMES[0].format( 170 | repo=args.repo, 171 | arch="{arch}", 172 | base_tag=args.tag, 173 | run_number=run_number, 174 | github_sha=github_sha, 175 | ) 176 | 177 | manifest_cmd_base = [manifest_base.format(arch=arch) for arch in ARCHS] 178 | 179 | return cls( 180 | names=preformatted_names, 181 | manifests=preformatted_manifests, 182 | ci_name=ci_name, 183 | manifest_base=manifest_base, 184 | manifest_cmd_base=manifest_cmd_base, 185 | ) 186 | 187 | 188 | def main(): 189 | """Run main script.""" 190 | args = parser.parse_args() 191 | docker_names = DockerNames.from_args(args) 192 | 193 | def run_command(*command, ignore_error: bool = False): 194 | """Run command and print output.""" 195 | print(f"$ {shlex.join(list(command))}") 196 | if not args.dry_run: 197 | rc = subprocess.call(list(command)) 198 | if rc != 0 and not ignore_error: 199 | print("Command failed") 200 | sys.exit(1) 201 | 202 | for name in docker_names.names: 203 | cmd = [ 204 | "docker", 205 | "tag", 206 | docker_names.ci_name, 207 | name, 208 | ] 209 | run_command(*cmd) 210 | cmd = ["docker", "push", name] 211 | run_command(*cmd) 212 | print(f"Published {name}") 213 | 214 | for manifest in docker_names.manifests: 215 | cmd = [ 216 | "docker", 217 | "manifest", 218 | "create", 219 | "--amend", 220 | manifest, 221 | *docker_names.manifest_cmd_base, 222 | ] 223 | run_command(*cmd) 224 | 225 | cmd = ["docker", "manifest", "push", "--purge", manifest] 226 | run_command(*cmd) 227 | print(f"Published {manifest}") 228 | 229 | 230 | if __name__ == "__main__": 231 | main() -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:experimental 2 | ARG ZM_VERSION=master 3 | ARG S6_ARCH=x86_64 4 | 5 | ##################################################################### 6 | # # 7 | # Download Zoneminder Source Code # 8 | # Parse control file for all runtime and build dependencies # 9 | # # 10 | ##################################################################### 11 | FROM python:alpine as zm-source 12 | ARG ZM_VERSION 13 | WORKDIR /zmsource 14 | 15 | RUN set -x \ 16 | && apk add \ 17 | git \ 18 | && git clone https://github.com/ZoneMinder/zoneminder.git . \ 19 | && git submodule update --init --recursive \ 20 | && git checkout ${ZM_VERSION} \ 21 | && git submodule update --init --recursive 22 | 23 | COPY parse_control.py . 24 | 25 | # This parses the control file located at distros/ubuntu2004/control 26 | # It outputs zoneminder_control which only includes requirements for zoneminder 27 | # This prevents equivs-build from being confused when there are multiple packages 28 | RUN set -x \ 29 | && python3 -u parse_control.py 30 | 31 | ##################################################################### 32 | # # 33 | # Convert rootfs to LF using dos2unix # 34 | # Alleviates issues when git uses CRLF on Windows # 35 | # # 36 | ##################################################################### 37 | FROM alpine:latest as rootfs-converter 38 | WORKDIR /rootfs 39 | 40 | RUN set -x \ 41 | && apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/ \ 42 | dos2unix 43 | 44 | COPY rootfs . 45 | RUN set -x \ 46 | && find . -type f -print0 | xargs -0 -n 1 -P 4 dos2unix \ 47 | && chmod -R +x * 48 | 49 | ##################################################################### 50 | # # 51 | # Download and extract s6 overlay # 52 | # # 53 | ##################################################################### 54 | FROM alpine:latest as s6downloader 55 | # Required to persist build arg 56 | ARG S6_ARCH 57 | WORKDIR /s6downloader 58 | 59 | RUN set -x \ 60 | && S6_OVERLAY_VERSION=$(wget --no-check-certificate -qO - https://api.github.com/repos/just-containers/s6-overlay/releases/latest | awk '/tag_name/{print $4;exit}' FS='[""]') \ 61 | && S6_OVERLAY_VERSION=${S6_OVERLAY_VERSION:1} \ 62 | && wget -O /tmp/s6-overlay-arch.tar.xz "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz" \ 63 | && wget -O /tmp/s6-overlay-noarch.tar.xz "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" \ 64 | && mkdir -p /tmp/s6 \ 65 | && tar -Jxvf /tmp/s6-overlay-noarch.tar.xz -C /tmp/s6 \ 66 | && tar -Jxvf /tmp/s6-overlay-arch.tar.xz -C /tmp/s6 \ 67 | && cp -r /tmp/s6/* . 68 | 69 | ##################################################################### 70 | # # 71 | # Prepare base-image with core programs + repository # 72 | # # 73 | ##################################################################### 74 | FROM debian:bookworm as base-image-core 75 | 76 | # Skip interactive post-install scripts 77 | ENV DEBIAN_FRONTEND=noninteractive 78 | 79 | RUN set -x \ 80 | && apt-get update \ 81 | && apt-get install -y --no-install-recommends \ 82 | ca-certificates \ 83 | gnupg \ 84 | wget \ 85 | && rm -rf /var/lib/apt/lists/* 86 | 87 | ##################################################################### 88 | # # 89 | # Build packages containing build and runtime dependencies # 90 | # for installation in later stages # 91 | # # 92 | ##################################################################### 93 | FROM base-image-core as package-builder 94 | WORKDIR /packages 95 | 96 | # Install base toolset 97 | RUN set -x \ 98 | && apt-get update \ 99 | && apt-get install -y \ 100 | devscripts 101 | 102 | COPY --from=zm-source /zmsource/zoneminder_control /tmp/control 103 | 104 | # Create runtime package 105 | RUN --mount=type=bind,target=/usr/share/equivs/template/debian/compat,source=/zmsource/zoneminder_compat,from=zm-source,rw \ 106 | set -x \ 107 | && equivs-build /tmp/control \ 108 | && ls | grep -P \(zoneminder_\)\(.*\)\(\.deb\) | xargs -I {} mv {} runtime-deps.deb 109 | 110 | # Create build-deps package 111 | RUN set -x \ 112 | && mk-build-deps /tmp/control \ 113 | && ls | grep -P \(build-deps\)\(.*\)\(\.deb\) | xargs -I {} mv {} build-deps.deb 114 | 115 | ##################################################################### 116 | # # 117 | # Install runtime dependencies # 118 | # Does not include shared lib dependencies as those are resolved # 119 | # after building. Installed in final-image # 120 | # # 121 | ##################################################################### 122 | FROM base-image-core as base-image 123 | 124 | # Install ZM Dependencies 125 | # Don't want recommends of ZM 126 | RUN --mount=type=bind,target=/tmp/runtime-deps.deb,source=/packages/runtime-deps.deb,from=package-builder,rw \ 127 | set -x \ 128 | && apt-get update \ 129 | && apt-get install -y --no-install-recommends \ 130 | ./tmp/runtime-deps.deb \ 131 | && rm -rf /var/lib/apt/lists/* 132 | 133 | # Remove "zoneminder" shim package from runtime-deps.deb and 134 | # set all runtime dependencies installed by package to manually installed 135 | # Allows removing individual packages without including all packages in autoremove 136 | RUN set -x \ 137 | && apt-get -y remove zoneminder \ 138 | && apt-mark manual $(apt-get -s autoremove 2>/dev/null | awk '/^Remv / { print $2 }') 139 | 140 | ##################################################################### 141 | # # 142 | # Install runtime + build dependencies # 143 | # Build Zoneminder # 144 | # # 145 | ##################################################################### 146 | FROM package-builder as builder 147 | WORKDIR /zmbuild 148 | # Yes WORKDIR is overwritten but this is more a comment 149 | # to specify the final WORKDIR 150 | 151 | # Install ZM Buid and Runtime Dependencies 152 | # Need to install runtime dependencies here as well 153 | # because we don't want devscripts in the base-image 154 | # This results in runtime dependencies being installed twice to avoid additional bloating 155 | WORKDIR /packages 156 | RUN set -x \ 157 | && apt-get update \ 158 | && apt-get install -y \ 159 | ./runtime-deps.deb \ 160 | ./build-deps.deb 161 | 162 | WORKDIR /zmbuild 163 | RUN --mount=type=bind,target=/zmbuild,source=/zmsource,from=zm-source,rw \ 164 | set -x \ 165 | && cmake \ 166 | -DCMAKE_INSTALL_PREFIX=/usr \ 167 | -DCMAKE_SKIP_RPATH=ON \ 168 | -DCMAKE_VERBOSE_MAKEFILE=OFF \ 169 | -DCMAKE_COLOR_MAKEFILE=ON \ 170 | -DZM_RUNDIR=/zoneminder/run \ 171 | -DZM_SOCKDIR=/zoneminder/run \ 172 | -DZM_TMPDIR=/zoneminder/tmp \ 173 | -DZM_LOGDIR=/zoneminder/logs \ 174 | -DZM_WEBDIR=/var/www/html \ 175 | -DZM_CONTENTDIR=/data \ 176 | -DZM_CACHEDIR=/zoneminder/cache \ 177 | -DZM_CGIDIR=/zoneminder/cgi-bin \ 178 | -DZM_WEB_USER=www-data \ 179 | -DZM_WEB_GROUP=www-data \ 180 | -DCMAKE_INSTALL_SYSCONFDIR=config \ 181 | -DZM_CONFIG_DIR=/config \ 182 | -DCMAKE_BUILD_TYPE=RelWithDebInfo \ 183 | . \ 184 | && make \ 185 | && make DESTDIR="/zminstall" install 186 | 187 | # Move default config location 188 | RUN set -x \ 189 | && mv /zminstall/config /zminstall/zoneminder/defaultconfig 190 | 191 | ##################################################################### 192 | # # 193 | # Install ZoneMinder # 194 | # Create required folders # 195 | # Install additional dependencies # 196 | # # 197 | ##################################################################### 198 | FROM base-image as final-build 199 | ARG ZM_VERSION 200 | 201 | # Add Nginx Repo 202 | RUN set -x \ 203 | && wget -qO - https://nginx.org/keys/nginx_signing.key | gpg --dearmor | tee /usr/share/keyrings/nginx.gpg > /dev/null \ 204 | && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/nginx.gpg] https://nginx.org/packages/mainline/debian/ bookworm nginx" > /etc/apt/sources.list.d/nginx.list 205 | 206 | # Install additional services required by ZM ("Recommends") 207 | # PHP-fpm not required for apache 208 | RUN set -x \ 209 | && apt-get update \ 210 | && apt-get install -y --no-install-recommends \ 211 | fcgiwrap \ 212 | mailutils \ 213 | msmtp \ 214 | nginx \ 215 | php-fpm \ 216 | tzdata \ 217 | && rm -rf /var/lib/apt/lists/* 218 | 219 | # Remove rsyslog as its unneeded and hangs the container on shutdown 220 | RUN set -x \ 221 | && apt-get -y remove rsyslog || true 222 | 223 | # Install ZM 224 | COPY --from=builder /zminstall / 225 | 226 | # Install s6 overlay 227 | COPY --from=s6downloader /s6downloader / 228 | 229 | # Copy rootfs 230 | COPY --from=rootfs-converter /rootfs / 231 | 232 | ## Create www-data user 233 | RUN set -x \ 234 | && groupmod -o -g 911 www-data \ 235 | && usermod -o -u 911 www-data 236 | 237 | # Reconfigure nginx and php logs 238 | # Configure msmtp 239 | RUN set -x \ 240 | && ln -sf /proc/self/fd/1 /var/log/nginx/access.log \ 241 | && ln -sf /proc/self/fd/1 /var/log/nginx/error.log \ 242 | && ln -sf /usr/bin/msmtp /usr/lib/sendmail \ 243 | && ln -sf /usr/bin/msmtp /usr/sbin/sendmail \ 244 | && rm -rf /etc/nginx/conf.d 245 | 246 | # Create required folders 247 | RUN set -x \ 248 | && mkdir -p \ 249 | /run/php \ 250 | /data \ 251 | /config \ 252 | /zoneminder/run \ 253 | /zoneminder/cache \ 254 | /zoneminder/logs \ 255 | /zoneminder/tmp \ 256 | /log \ 257 | && chown -R www-data:www-data \ 258 | /data \ 259 | /config \ 260 | /zoneminder \ 261 | && chmod -R 755 \ 262 | /data \ 263 | /config \ 264 | /zoneminder \ 265 | /log \ 266 | && chown -R nobody:nogroup \ 267 | /log 268 | 269 | # System Variables 270 | ENV \ 271 | S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \ 272 | S6_FIX_ATTRS_HIDDEN=1 \ 273 | S6_BEHAVIOUR_IF_STAGE2_FAILS=2 \ 274 | MAX_LOG_SIZE_BYTES=1000000 \ 275 | MAX_LOG_NUMBER=10 276 | 277 | # Default User Variables 278 | ENV \ 279 | MYSQL_HOST=db \ 280 | PHP_MAX_CHILDREN=120 \ 281 | PHP_START_SERVERS=12 \ 282 | PHP_MIN_SPARE_SERVERS=6 \ 283 | PHP_MAX_SPARE_SERVERS=18 \ 284 | PHP_MEMORY_LIMIT=2048M \ 285 | PHP_MAX_EXECUTION_TIME=600 \ 286 | PHP_MAX_INPUT_VARIABLES=3000 \ 287 | PHP_MAX_INPUT_TIME=600 \ 288 | FCGIWRAP_PROCESSES=15 \ 289 | FASTCGI_BUFFERS_CONFIGURATION_STRING="64 4K" \ 290 | PUID=911 \ 291 | PGID=911 \ 292 | TZ="America/Chicago" \ 293 | USE_SECURE_RANDOM_ORG=1 294 | 295 | LABEL \ 296 | com.github.alexyao2015.zoneminder_version=${ZM_VERSION} 297 | 298 | EXPOSE 80/tcp 299 | 300 | CMD ["/init"] 301 | -------------------------------------------------------------------------------- /.github/workflows/docker-build.yaml: -------------------------------------------------------------------------------- 1 | name: Docker Build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * *' 8 | 9 | jobs: 10 | getversion: 11 | runs-on: ubuntu-latest 12 | name: Obtain ZoneMinder Version 13 | outputs: 14 | build-version: ${{ steps.set-version.outputs.zmversion }} 15 | steps: 16 | - name: Set ZoneMinder Build Version 17 | id: set-version 18 | run: | 19 | if [[ ${GITHUB_REF} == refs/heads/* || ${GITHUB_REF} == refs/pull/* ]]; then 20 | # Build from latest ZoneMinder commit 21 | ZM_VERSION=$(wget \ 22 | -qO - https://api.github.com/repos/ZoneMinder/zoneminder/commits/master \ 23 | | awk '/sha/{print $4;exit}' FS='[""]') 24 | else 25 | # Build tag 26 | ZM_VERSION=${GITHUB_REF##*/} 27 | fi 28 | echo Building ZoneMinder ${ZM_VERSION} 29 | echo "zmversion=${ZM_VERSION}" >> $GITHUB_OUTPUT 30 | 31 | build: 32 | name: Build Docker Image 33 | runs-on: ubuntu-latest 34 | if: ${{ needs.getversion.outputs.build-version != '' }} 35 | needs: getversion 36 | env: 37 | ZM_VERSION: ${{ needs.getversion.outputs.build-version }} 38 | strategy: 39 | matrix: 40 | include: 41 | - arch: linux/386 42 | arch_friendly: i386 43 | s6_arch: i686 44 | - arch: linux/amd64 45 | arch_friendly: amd64 46 | s6_arch: x86_64 47 | - arch: linux/arm/v7 48 | arch_friendly: armv7 49 | s6_arch: armhf 50 | - arch: linux/arm64 51 | arch_friendly: arm64 52 | s6_arch: aarch64 53 | 54 | steps: 55 | - name: Checkout 56 | uses: actions/checkout@v6 57 | 58 | - name: Set up QEMU 59 | uses: docker/setup-qemu-action@v3 60 | 61 | - name: Set up Docker Buildx 62 | uses: docker/setup-buildx-action@v3 63 | with: 64 | driver-opts: | 65 | env.BUILDKIT_STEP_LOG_MAX_SIZE=10000000 66 | env.BUILDKIT_STEP_LOG_MAX_SPEED=100000000 67 | install: true 68 | 69 | - name: Cache Docker layers 70 | uses: actions/cache@v5 71 | continue-on-error: true 72 | with: 73 | path: /tmp/.buildx-cache 74 | key: ${{ matrix.arch }}-${{ env.ZM_VERSION }}-${{ github.sha }} 75 | restore-keys: | 76 | ${{ matrix.arch }}-${{ env.ZM_VERSION }}- 77 | 78 | - name: Build ZoneMinder 79 | run: | 80 | set -x 81 | docker build \ 82 | --build-arg ZM_VERSION=${ZM_VERSION} \ 83 | --build-arg S6_ARCH=${{ matrix.s6_arch }} \ 84 | --tag ci:${{ github.run_number }} \ 85 | --platform ${{ matrix.arch }} \ 86 | --progress plain \ 87 | --file ./Dockerfile \ 88 | --cache-from type=local,src=/tmp/.buildx-cache \ 89 | --cache-to type=local,dest=/tmp/.buildx-cache-new \ 90 | --load \ 91 | . 92 | 93 | # Temp fix 94 | # https://github.com/docker/build-push-action/issues/252 95 | # https://github.com/moby/buildkit/issues/1896 96 | - name: Move cache 97 | run: | 98 | rm -rf /tmp/.buildx-cache 99 | mv /tmp/.buildx-cache-new /tmp/.buildx-cache 100 | 101 | - name: Inspect 102 | run: | 103 | set -x 104 | docker image inspect ci:${{ github.run_number }} 105 | 106 | - name: Save tarball 107 | run: | 108 | set -x 109 | docker save ci:${{ github.run_number }} | gzip > ci-${{ matrix.arch_friendly }}-${{ github.run_number }}.tar.gz 110 | 111 | - name: Upload Artifact 112 | uses: actions/upload-artifact@v6 113 | with: 114 | name: ci-${{ matrix.arch_friendly }}-${{ github.run_number }} 115 | path: ci-${{ matrix.arch_friendly }}-${{ github.run_number }}.tar.gz 116 | 117 | test: 118 | needs: build 119 | name: Test Image 120 | runs-on: ubuntu-latest 121 | strategy: 122 | matrix: 123 | arch: 124 | - i386 125 | - amd64 126 | - armv7 127 | - arm64 128 | steps: 129 | - name: Checkout 130 | uses: actions/checkout@v6 131 | 132 | - name: Set up QEMU 133 | uses: docker/setup-qemu-action@v3 134 | 135 | - name: Download container artifact 136 | uses: actions/download-artifact@v7 137 | with: 138 | name: ci-${{ matrix.arch }}-${{ github.run_number }} 139 | 140 | - name: Import image 141 | run: | 142 | docker load --input ci-${{ matrix.arch }}-${{ github.run_number }}.tar.gz 143 | docker tag ci:${{ github.run_number }} ci:latest 144 | 145 | # Fails if zoneminder is not up 146 | - name: Start image twice 147 | timeout-minutes: 5 148 | run: | 149 | set -x 150 | docker compose -f docker-compose.test.yml up & 151 | sleep 60 152 | if [ ! "$(docker ps -q -f name=zoneminder)" ]; then 153 | exit 1 154 | fi 155 | docker compose -f docker-compose.test.yml down 156 | docker compose -f docker-compose.test.yml up & 157 | sleep 60 158 | docker compose -f docker-compose.test.yml down 159 | 160 | # Fails if zoneminder fails to stop normally 161 | - name: Start image and stop zoneminder 162 | timeout-minutes: 5 163 | run: | 164 | set -x 165 | docker compose -f docker-compose.test.yml up & 166 | sleep 60 167 | docker stop zoneminder 168 | docker compose -f docker-compose.test.yml down 169 | 170 | # Fails if zoneminder doesn't stop when db is down 171 | - name: Start image and stop db 172 | timeout-minutes: 5 173 | run: | 174 | set -x 175 | docker compose -f docker-compose.test.yml up & 176 | sleep 120 177 | docker stop db 178 | sleep 60 179 | if [ "$(docker ps -q -f name=zoneminder)" ]; then 180 | exit 1 181 | fi 182 | 183 | release: 184 | needs: 185 | - getversion 186 | - test 187 | name: Upload Release Asset 188 | if: ${{ startsWith(github.ref, 'refs/tags/') }} 189 | runs-on: ubuntu-latest 190 | steps: 191 | - name: Download container artifact 192 | uses: actions/download-artifact@v7 193 | 194 | - name: Upload Release Asset 195 | uses: softprops/action-gh-release@v2 196 | with: 197 | files: ci-*/ci-*.tar.gz 198 | body: Automated release of ZoneMinder v${{ needs.getversion.outputs.build-version }} 199 | draft: false 200 | env: 201 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 202 | 203 | publish: 204 | name: Publish Image 205 | runs-on: ubuntu-latest 206 | needs: 207 | - getversion 208 | - test 209 | if: ${{ !startsWith(github.ref, 'refs/pull/') && !startsWith(github.ref, 'refs/heads/dependabot/') }} 210 | env: 211 | ZM_VERSION: ${{ needs.getversion.outputs.build-version }} 212 | strategy: 213 | matrix: 214 | arch: 215 | - i386 216 | - amd64 217 | - armv7 218 | - arm64 219 | registry: 220 | - { 221 | url: "https://index.docker.io/v1/", 222 | username: DOCKER_USERNAME, 223 | password: DOCKER_PASSWORD, 224 | repo: yaoa/zoneminder-base 225 | } 226 | - { 227 | url: ghcr.io/zoneminder-containers, 228 | username: GCHR_USERNAME, 229 | password: GHCR_PAT, 230 | repo: ghcr.io/zoneminder-containers/zoneminder-base 231 | } 232 | steps: 233 | - name: Checkout 234 | uses: actions/checkout@v6 235 | 236 | - name: Download container artifact 237 | uses: actions/download-artifact@v7 238 | with: 239 | name: ci-${{ matrix.arch }}-${{ github.run_number }} 240 | 241 | - name: Import image 242 | run: | 243 | docker load --input ci-${{ matrix.arch }}-${{ github.run_number }}.tar.gz 244 | 245 | - name: Docker login 246 | run: | 247 | docker login ${{ matrix.registry.url }} -u ${{ secrets[matrix.registry.username] }} -p ${{ secrets[matrix.registry.password] }} 248 | 249 | # Main gets pushed to branch name and nightly 250 | # Tags get latest and ref name (aka the tag name) 251 | 252 | # push to ref name 253 | - name: Push image (ref) 254 | if: ${{ startsWith(github.ref, 'refs/heads/') }} 255 | run: | 256 | ./publish.py \ 257 | --tag ${GITHUB_REF##*/} \ 258 | --repo ${{ matrix.registry.repo }} \ 259 | --image \ 260 | --github-sha ${ZM_VERSION} \ 261 | --run-number ${{ github.run_number }} \ 262 | --arch ${{ matrix.arch }} \ 263 | --image-name ci:${{ github.run_number }} 264 | 265 | # push main branch to nightly tag 266 | - name: Push image (nightly) 267 | if: ${{ github.ref == 'refs/heads/main' }} 268 | run: | 269 | ./publish.py \ 270 | --tag nightly \ 271 | --repo ${{ matrix.registry.repo }} \ 272 | --image \ 273 | --github-sha ${ZM_VERSION} \ 274 | --run-number ${{ github.run_number }} \ 275 | --arch ${{ matrix.arch }} \ 276 | --image-name ci:${{ github.run_number }} 277 | 278 | # if its tagged, push to tag name and latest 279 | - name: Push image (tag) 280 | if: ${{ startsWith(github.ref, 'refs/tags/') }} 281 | run: | 282 | ./publish.py \ 283 | --tag ${GITHUB_REF##*/} \ 284 | --repo ${{ matrix.registry.repo }} \ 285 | --image \ 286 | --github-sha ${ZM_VERSION} \ 287 | --run-number ${{ github.run_number }} \ 288 | --arch ${{ matrix.arch }} \ 289 | --image-name ci:${{ github.run_number }} \ 290 | --latest 291 | 292 | # if its tagged, push to release 293 | - name: Push image (tag) 294 | if: ${{ startsWith(github.ref, 'refs/tags/') }} 295 | run: | 296 | ./publish.py \ 297 | --tag release \ 298 | --repo ${{ matrix.registry.repo }} \ 299 | --image \ 300 | --github-sha ${ZM_VERSION} \ 301 | --run-number ${{ github.run_number }} \ 302 | --arch ${{ matrix.arch }} \ 303 | --image-name ci:${{ github.run_number }} 304 | 305 | create_manifest: 306 | name: Create Manifest 307 | runs-on: ubuntu-latest 308 | needs: 309 | - getversion 310 | - publish 311 | env: 312 | ZM_VERSION: ${{ needs.getversion.outputs.build-version }} 313 | DOCKER_CLI_EXPERIMENTAL: "enabled" 314 | strategy: 315 | matrix: 316 | registry: 317 | - { 318 | url: "https://index.docker.io/v1/", 319 | username: DOCKER_USERNAME, 320 | password: DOCKER_PASSWORD, 321 | repo: yaoa/zoneminder-base 322 | } 323 | - { 324 | url: ghcr.io/zoneminder-containers, 325 | username: GCHR_USERNAME, 326 | password: GHCR_PAT, 327 | repo: ghcr.io/zoneminder-containers/zoneminder-base 328 | } 329 | steps: 330 | - name: Checkout 331 | uses: actions/checkout@v6 332 | 333 | - name: Docker login 334 | run: | 335 | docker login ${{ matrix.registry.url }} -u ${{ secrets[matrix.registry.username] }} -p ${{ secrets[matrix.registry.password] }} 336 | 337 | # Main gets pushed to branch name and nightly 338 | # Tags get latest and ref name (aka the tag name) 339 | 340 | # push to ref name 341 | - name: Push image (ref) 342 | if: ${{ startsWith(github.ref, 'refs/heads/') }} 343 | run: | 344 | ./publish.py \ 345 | --tag ${GITHUB_REF##*/} \ 346 | --repo ${{ matrix.registry.repo }} \ 347 | --manifest \ 348 | --github-sha ${ZM_VERSION} \ 349 | --run-number ${{ github.run_number }} 350 | 351 | # push main branch to nightly tag 352 | - name: Push image (nightly) 353 | if: ${{ github.ref == 'refs/heads/main' }} 354 | run: | 355 | ./publish.py \ 356 | --tag nightly \ 357 | --repo ${{ matrix.registry.repo }} \ 358 | --manifest \ 359 | --github-sha ${ZM_VERSION} \ 360 | --run-number ${{ github.run_number }} 361 | 362 | # if its tagged, push to tag name and latest 363 | - name: Push image (tag) 364 | if: ${{ startsWith(github.ref, 'refs/tags/') }} 365 | run: | 366 | ./publish.py \ 367 | --tag ${GITHUB_REF##*/} \ 368 | --repo ${{ matrix.registry.repo }} \ 369 | --manifest \ 370 | --github-sha ${ZM_VERSION} \ 371 | --run-number ${{ github.run_number }} \ 372 | --latest 373 | 374 | # if its tagged, push to release 375 | - name: Push image (tag) 376 | if: ${{ startsWith(github.ref, 'refs/tags/') }} 377 | run: | 378 | ./publish.py \ 379 | --tag release \ 380 | --repo ${{ matrix.registry.repo }} \ 381 | --manifest \ 382 | --github-sha ${ZM_VERSION} \ 383 | --run-number ${{ github.run_number }} 384 | 385 | dispatch: 386 | name: Dispatch event to eventserver-base 387 | runs-on: ubuntu-latest 388 | needs: 389 | - getversion 390 | - create_manifest 391 | steps: 392 | - name: Trigger ES Build 393 | run: | 394 | curl -XPOST \ 395 | -u "${{ secrets.GCHR_USERNAME}}:${{secrets.GHCR_PAT}}" \ 396 | -H "Accept: application/vnd.github.everest-preview+json" \ 397 | -H "Content-Type: application/json" https://api.github.com/repos/zoneminder-containers/eventserver-base/dispatches \ 398 | --data '{"event_type": "build_image", "client_payload": {"zm_version": "${{ needs.getversion.outputs.build-version }}", "tagged": "${{ startsWith(github.ref, 'refs/tags/') }}"}}' 399 | echo "Dispatch Successful" 400 | --------------------------------------------------------------------------------