فهرست منبع

[PHP-FPM] use python bootstrapper to start PHP-FPM container

FreddleSpl0it 3 ماه پیش
والد
کامیت
f329549c2e

+ 5 - 0
.gitignore

@@ -31,6 +31,11 @@ data/conf/nginx/*.bak
 data/conf/nginx/*.conf
 data/conf/nginx/*.conf
 data/conf/nginx/*.custom
 data/conf/nginx/*.custom
 data/conf/phpfpm/sogo-sso/sogo-sso.pass
 data/conf/phpfpm/sogo-sso/sogo-sso.pass
+data/conf/phpfpm/opcache-recommended.ini
+data/conf/phpfpm/other.ini
+data/conf/phpfpm/pools.conf
+data/conf/phpfpm/session_store.ini
+data/conf/phpfpm/upload.ini
 data/conf/portainer/
 data/conf/portainer/
 data/conf/postfix/allow_mailcow_local.regexp
 data/conf/postfix/allow_mailcow_local.regexp
 data/conf/postfix/custom_postscreen_whitelist.cidr
 data/conf/postfix/custom_postscreen_whitelist.cidr

+ 2 - 0
data/Dockerfiles/bootstrap/main.py

@@ -25,6 +25,8 @@ def main():
     from modules.BootstrapClamd import Bootstrap
     from modules.BootstrapClamd import Bootstrap
   elif container_name == "mysql-mailcow":
   elif container_name == "mysql-mailcow":
     from modules.BootstrapMysql import Bootstrap
     from modules.BootstrapMysql import Bootstrap
+  elif container_name == "php-fpm-mailcow":
+    from modules.BootstrapPhpfpm import Bootstrap
   else:
   else:
     print(f"No bootstrap handler for container: {container_name}", file=sys.stderr)
     print(f"No bootstrap handler for container: {container_name}", file=sys.stderr)
     sys.exit(1)
     sys.exit(1)

+ 207 - 0
data/Dockerfiles/bootstrap/modules/BootstrapPhpfpm.py

@@ -0,0 +1,207 @@
+from jinja2 import Environment, FileSystemLoader
+from modules.BootstrapBase import BootstrapBase
+from pathlib import Path
+import os
+import ipaddress
+import sys
+import time
+import platform
+import subprocess
+
+class Bootstrap(BootstrapBase):
+  def bootstrap(self):
+    self.connect_mysql()
+    self.connect_redis()
+
+    # Setup Jinja2 Environment and load vars
+    self.env = Environment(
+      loader=FileSystemLoader('/php-conf/config_templates'),
+      keep_trailing_newline=True,
+      lstrip_blocks=True,
+      trim_blocks=True
+    )
+    extra_vars = {
+    }
+    self.env_vars = self.prepare_template_vars('/overwrites.json', extra_vars)
+
+    print("Set Timezone")
+    self.set_timezone()
+
+    # Prepare Redis and MySQL Database
+    # TODO: move to dockerapi
+    if self.isYes(os.getenv("MASTER", "")):
+      print("We are master, preparing...")
+      self.prepare_redis()
+      self.setup_apikeys(
+        os.getenv("API_ALLOW_FROM", "").strip(),
+        os.getenv("API_KEY", "").strip(),
+        os.getenv("API_KEY_READ_ONLY", "").strip()
+      )
+      self.setup_mysql_events()
+
+
+    print("Render config")
+    self.render_config("opcache-recommended.ini.j2", "/usr/local/etc/php/conf.d/opcache-recommended.ini")
+    self.render_config("pools.conf.j2", "/usr/local/etc/php-fpm.d/z-pools.conf")
+    self.render_config("other.ini.j2", "/usr/local/etc/php/conf.d/zzz-other.ini")
+    self.render_config("upload.ini.j2", "/usr/local/etc/php/conf.d/upload.ini")
+    self.render_config("session_store.ini.j2", "/usr/local/etc/php/conf.d/session_store.ini")
+    self.render_config("0081-custom-mailcow.css.j2", "/web/css/build/0081-custom-mailcow.css")
+
+    self.copy_file("/usr/local/etc/php/conf.d/opcache-recommended.ini", "/php-conf/opcache-recommended.ini")
+    self.copy_file("/usr/local/etc/php-fpm.d/z-pools.conf", "/php-conf/pools.conf")
+    self.copy_file("/usr/local/etc/php/conf.d/zzz-other.ini", "/php-conf/other.ini")
+    self.copy_file("/usr/local/etc/php/conf.d/upload.ini", "/php-conf/upload.ini")
+    self.copy_file("/usr/local/etc/php/conf.d/session_store.ini", "/php-conf/session_store.ini")
+
+    self.set_owner("/global_sieve", 82, 82, recursive=True)
+    self.set_owner("/web/templates/cache", 82, 82, recursive=True)
+    self.remove("/web/templates/cache", wipe_contents=True, exclude=[".gitkeep"])
+
+    print("Running DB init...")
+    self.run_command(["php", "-c", "/usr/local/etc/php", "-f", "/web/inc/init_db.inc.php"], check=False)
+
+  def prepare_redis(self):
+    print("Setting default Redis keys if missing...")
+
+    # Q_RELEASE_FORMAT
+    if self.redis_conn.get("Q_RELEASE_FORMAT") is None:
+      self.redis_conn.set("Q_RELEASE_FORMAT", "raw")
+
+    # Q_MAX_AGE
+    if self.redis_conn.get("Q_MAX_AGE") is None:
+      self.redis_conn.set("Q_MAX_AGE", 365)
+
+    # PASSWD_POLICY hash defaults
+    if self.redis_conn.hget("PASSWD_POLICY", "length") is None:
+      self.redis_conn.hset("PASSWD_POLICY", mapping={
+        "length": 6,
+        "chars": 0,
+        "special_chars": 0,
+        "lowerupper": 0,
+        "numbers": 0
+      })
+
+    # DOMAIN_MAP
+    print("Rebuilding DOMAIN_MAP from MySQL...")
+    self.redis_conn.delete("DOMAIN_MAP")
+    domains = set()
+    try:
+      cursor = self.mysql_conn.cursor()
+
+      cursor.execute("SELECT domain FROM domain")
+      domains.update(row[0] for row in cursor.fetchall())
+      cursor.execute("SELECT alias_domain FROM alias_domain")
+      domains.update(row[0] for row in cursor.fetchall())
+
+      cursor.close()
+
+      if domains:
+        for domain in domains:
+          self.redis_conn.hset("DOMAIN_MAP", domain, 1)
+        print(f"{len(domains)} domains added to DOMAIN_MAP.")
+      else:
+        print("No domains found to insert into DOMAIN_MAP.")
+    except Exception as e:
+      print(f"Failed to rebuild DOMAIN_MAP: {e}")
+
+  def setup_apikeys(self, api_allow_from, api_key_rw, api_key_ro):
+    if not api_allow_from or api_allow_from == "invalid":
+      return
+
+    print("Validating API_ALLOW_FROM IPs...")
+    ip_list = [ip.strip() for ip in api_allow_from.split(",")]
+    validated_ips = []
+
+    for ip in ip_list:
+      try:
+        ipaddress.ip_network(ip, strict=False)
+        validated_ips.append(ip)
+      except ValueError:
+        continue
+    if not validated_ips:
+      print("No valid IPs found in API_ALLOW_FROM")
+      return
+
+    allow_from_str = ",".join(validated_ips)
+    cursor = self.mysql_conn.cursor()
+    try:
+      if api_key_rw and api_key_rw != "invalid":
+        print("Setting RW API key...")
+        cursor.execute("DELETE FROM api WHERE access = 'rw'")
+        cursor.execute(
+          "INSERT INTO api (api_key, active, allow_from, access) VALUES (%s, %s, %s, %s)",
+          (api_key_rw, 1, allow_from_str, "rw")
+        )
+
+      if api_key_ro and api_key_ro != "invalid":
+        print("Setting RO API key...")
+        cursor.execute("DELETE FROM api WHERE access = 'ro'")
+        cursor.execute(
+          "INSERT INTO api (api_key, active, allow_from, access) VALUES (%s, %s, %s, %s)",
+          (api_key_ro, 1, allow_from_str, "ro")
+        )
+
+      self.mysql_conn.commit()
+      print("API key(s) set successfully.")
+    except Exception as e:
+      print(f"Failed to configure API keys: {e}")
+      self.mysql_conn.rollback()
+    finally:
+      cursor.close()
+
+  def setup_mysql_events(self):
+    print("Creating scheduled MySQL EVENTS...")
+
+    queries = [
+      "DROP EVENT IF EXISTS clean_spamalias;",
+      """
+      CREATE EVENT clean_spamalias
+      ON SCHEDULE EVERY 1 DAY
+      DO
+      DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP();
+      """,
+      "DROP EVENT IF EXISTS clean_oauth2;",
+      """
+      CREATE EVENT clean_oauth2
+      ON SCHEDULE EVERY 1 DAY
+      DO
+      BEGIN
+        DELETE FROM oauth_refresh_tokens WHERE expires < NOW();
+        DELETE FROM oauth_access_tokens WHERE expires < NOW();
+        DELETE FROM oauth_authorization_codes WHERE expires < NOW();
+      END;
+      """,
+      "DROP EVENT IF EXISTS clean_sasl_log;",
+      """
+      CREATE EVENT clean_sasl_log
+      ON SCHEDULE EVERY 1 DAY
+      DO
+      BEGIN
+        DELETE sasl_log.* FROM sasl_log
+          LEFT JOIN (
+            SELECT username, service, MAX(datetime) AS lastdate
+            FROM sasl_log
+            GROUP BY username, service
+          ) AS last
+          ON sasl_log.username = last.username AND sasl_log.service = last.service
+          WHERE datetime < DATE_SUB(NOW(), INTERVAL 31 DAY)
+            AND datetime < lastdate;
+
+        DELETE FROM sasl_log
+          WHERE username NOT IN (SELECT username FROM mailbox)
+            AND datetime < DATE_SUB(NOW(), INTERVAL 31 DAY);
+      END;
+      """
+    ]
+
+    try:
+      cursor = self.mysql_conn.cursor()
+      for query in queries:
+        cursor.execute(query)
+      self.mysql_conn.commit()
+      cursor.close()
+      print("MySQL EVENTS created successfully.")
+    except Exception as e:
+      print(f"Failed to create MySQL EVENTS: {e}")
+      self.mysql_conn.rollback()

+ 14 - 3
data/Dockerfiles/phpfpm/Dockerfile

@@ -63,6 +63,7 @@ RUN apk add -U --no-cache autoconf \
   samba-client \
   samba-client \
   zlib-dev \
   zlib-dev \
   tzdata \
   tzdata \
+  python3 py3-pip \
   && pecl install APCu-${APCU_PECL_VERSION} \
   && pecl install APCu-${APCU_PECL_VERSION} \
   && pecl install imagick-${IMAGICK_PECL_VERSION} \
   && pecl install imagick-${IMAGICK_PECL_VERSION} \
   && pecl install mailparse-${MAILPARSE_PECL_VERSION} \
   && pecl install mailparse-${MAILPARSE_PECL_VERSION} \
@@ -72,7 +73,7 @@ RUN apk add -U --no-cache autoconf \
   && pecl clear-cache \
   && pecl clear-cache \
   && docker-php-ext-configure intl \
   && docker-php-ext-configure intl \
   && docker-php-ext-configure exif \
   && docker-php-ext-configure exif \
-  && docker-php-ext-configure gd --with-freetype=/usr/include/ \  
+  && docker-php-ext-configure gd --with-freetype=/usr/include/ \
     --with-jpeg=/usr/include/ \
     --with-jpeg=/usr/include/ \
     --with-webp \
     --with-webp \
     --with-xpm \
     --with-xpm \
@@ -107,8 +108,18 @@ RUN apk add -U --no-cache autoconf \
     pcre-dev \
     pcre-dev \
     zlib-dev
     zlib-dev
 
 
-COPY ./docker-entrypoint.sh /
+RUN pip install  --break-system-packages \
+  mysql-connector-python \
+  jinja2 \
+  redis \
+  dnspython
 
 
-ENTRYPOINT ["/docker-entrypoint.sh"]
 
 
+COPY data/Dockerfiles/bootstrap /bootstrap
+COPY data/Dockerfiles/phpfpm/docker-entrypoint.sh /
+
+RUN chmod +x /docker-entrypoint.sh
+
+
+ENTRYPOINT ["/docker-entrypoint.sh"]
 CMD ["php-fpm"]
 CMD ["php-fpm"]

+ 9 - 214
data/Dockerfiles/phpfpm/docker-entrypoint.sh

@@ -1,219 +1,5 @@
 #!/bin/bash
 #!/bin/bash
 
 
-function array_by_comma { local IFS=","; echo "$*"; }
-
-# Wait for containers
-while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
-  echo "Waiting for SQL..."
-  sleep 2
-done
-
-# Do not attempt to write to slave
-if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
-  REDIS_HOST=$REDIS_SLAVEOF_IP
-  REDIS_PORT=$REDIS_SLAVEOF_PORT
-else
-  REDIS_HOST="redis"
-  REDIS_PORT="6379"
-fi
-REDIS_CMDLINE="redis-cli -h ${REDIS_HOST} -p ${REDIS_PORT} -a ${REDISPASS} --no-auth-warning"
-
-until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
-  echo "Waiting for Redis..."
-  sleep 2
-done
-
-# Set redis session store
-echo -n '
-session.save_handler = redis
-session.save_path = "tcp://'${REDIS_HOST}':'${REDIS_PORT}'?auth='${REDISPASS}'"
-' > /usr/local/etc/php/conf.d/session_store.ini
-
-# Check mysql_upgrade (master and slave)
-CONTAINER_ID=
-until [[ ! -z "${CONTAINER_ID}" ]] && [[ "${CONTAINER_ID}" =~ ^[[:alnum:]]*$ ]]; do
-  CONTAINER_ID=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"mysql-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null)
-  echo "Could not get mysql-mailcow container id... trying again"
-  sleep 2
-done
-echo "MySQL @ ${CONTAINER_ID}"
-SQL_LOOP_C=0
-SQL_CHANGED=0
-until [[ ${SQL_UPGRADE_STATUS} == 'success' ]]; do
-  if [ ${SQL_LOOP_C} -gt 4 ]; then
-    echo "Tried to upgrade MySQL and failed, giving up after ${SQL_LOOP_C} retries and starting container (oops, not good)"
-    break
-  fi
-  SQL_FULL_UPGRADE_RETURN=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_upgrade"}' --silent -H 'Content-type: application/json')
-  SQL_UPGRADE_STATUS=$(echo ${SQL_FULL_UPGRADE_RETURN} | jq -r .type)
-  SQL_LOOP_C=$((SQL_LOOP_C+1))
-  echo "SQL upgrade iteration #${SQL_LOOP_C}"
-  if [[ ${SQL_UPGRADE_STATUS} == 'warning' ]]; then
-    SQL_CHANGED=1
-    echo "MySQL applied an upgrade, debug output:"
-    echo ${SQL_FULL_UPGRADE_RETURN}
-    sleep 3
-    while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
-      echo "Waiting for SQL to return, please wait"
-      sleep 2
-    done
-    continue
-  elif [[ ${SQL_UPGRADE_STATUS} == 'success' ]]; then
-    echo "MySQL is up-to-date - debug output:"
-    echo ${SQL_FULL_UPGRADE_RETURN}
-  else
-    echo "No valid reponse for mysql_upgrade was received, debug output:"
-    echo ${SQL_FULL_UPGRADE_RETURN}
-  fi
-done
-
-# doing post-installation stuff, if SQL was upgraded (master and slave)
-if [ ${SQL_CHANGED} -eq 1 ]; then
-  POSTFIX=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null)
-  if [[ -z "${POSTFIX}" ]] || ! [[ "${POSTFIX}" =~ ^[[:alnum:]]*$ ]]; then
-    echo "Could not determine Postfix container ID, skipping Postfix restart."
-  else
-    echo "Restarting Postfix"
-    curl -X POST --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${POSTFIX}/restart | jq -r '.msg'
-    echo "Sleeping 5 seconds..."
-    sleep 5
-  fi
-fi
-
-# Check mysql tz import (master and slave)
-TZ_CHECK=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT CONVERT_TZ('2019-11-02 23:33:00','Europe/Berlin','UTC') AS time;" -BN 2> /dev/null)
-if [[ -z ${TZ_CHECK} ]] || [[ "${TZ_CHECK}" == "NULL" ]]; then
-  SQL_FULL_TZINFO_IMPORT_RETURN=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_tzinfo_to_sql"}' --silent -H 'Content-type: application/json')
-  echo "MySQL mysql_tzinfo_to_sql - debug output:"
-  echo ${SQL_FULL_TZINFO_IMPORT_RETURN}
-fi
-
-if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
-  echo "We are master, preparing..."
-  # Set a default release format
-  if [[ -z $(${REDIS_CMDLINE} --raw GET Q_RELEASE_FORMAT) ]]; then
-    ${REDIS_CMDLINE} --raw SET Q_RELEASE_FORMAT raw
-  fi
-
-  # Set max age of q items - if unset
-  if [[ -z $(${REDIS_CMDLINE} --raw GET Q_MAX_AGE) ]]; then
-    ${REDIS_CMDLINE} --raw SET Q_MAX_AGE 365
-  fi
-
-  # Set default password policy - if unset
-  if [[ -z $(${REDIS_CMDLINE} --raw HGET PASSWD_POLICY length) ]]; then
-    ${REDIS_CMDLINE} --raw HSET PASSWD_POLICY length 6
-    ${REDIS_CMDLINE} --raw HSET PASSWD_POLICY chars 0
-    ${REDIS_CMDLINE} --raw HSET PASSWD_POLICY special_chars 0
-    ${REDIS_CMDLINE} --raw HSET PASSWD_POLICY lowerupper 0
-    ${REDIS_CMDLINE} --raw HSET PASSWD_POLICY numbers 0
-  fi
-
-  # Trigger db init
-  echo "Running DB init..."
-  php -c /usr/local/etc/php -f /web/inc/init_db.inc.php
-
-  # Recreating domain map
-  echo "Rebuilding domain map in Redis..."
-  declare -a DOMAIN_ARR
-    ${REDIS_CMDLINE} DEL DOMAIN_MAP > /dev/null
-  while read line
-  do
-    DOMAIN_ARR+=("$line")
-  done < <(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain" -Bs)
-  while read line
-  do
-    DOMAIN_ARR+=("$line")
-  done < <(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT alias_domain FROM alias_domain" -Bs)
-
-  if [[ ! -z ${DOMAIN_ARR} ]]; then
-  for domain in "${DOMAIN_ARR[@]}"; do
-    ${REDIS_CMDLINE} HSET DOMAIN_MAP ${domain} 1 > /dev/null
-  done
-  fi
-
-  # Set API options if env vars are not empty
-  if [[ ${API_ALLOW_FROM} != "invalid" ]] && [[ ! -z ${API_ALLOW_FROM} ]]; then
-    IFS=',' read -r -a API_ALLOW_FROM_ARR <<< "${API_ALLOW_FROM}"
-    declare -a VALIDATED_API_ALLOW_FROM_ARR
-    REGEX_IP6='^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}(/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$'
-    REGEX_IP4='^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(/([0-9]|[1-2][0-9]|3[0-2]))?$'
-    for IP in "${API_ALLOW_FROM_ARR[@]}"; do
-      if [[ ${IP} =~ ${REGEX_IP6} ]] || [[ ${IP} =~ ${REGEX_IP4} ]]; then
-        VALIDATED_API_ALLOW_FROM_ARR+=("${IP}")
-      fi
-    done
-    VALIDATED_IPS=$(array_by_comma ${VALIDATED_API_ALLOW_FROM_ARR[*]})
-    if [[ ! -z ${VALIDATED_IPS} ]]; then
-      if [[ ${API_KEY} != "invalid" ]] && [[ ! -z ${API_KEY} ]]; then
-        mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
-DELETE FROM api WHERE access = 'rw';
-INSERT INTO api (api_key, active, allow_from, access) VALUES ("${API_KEY}", "1", "${VALIDATED_IPS}", "rw");
-EOF
-      fi
-      if [[ ${API_KEY_READ_ONLY} != "invalid" ]] && [[ ! -z ${API_KEY_READ_ONLY} ]]; then
-        mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
-DELETE FROM api WHERE access = 'ro';
-INSERT INTO api (api_key, active, allow_from, access) VALUES ("${API_KEY_READ_ONLY}", "1", "${VALIDATED_IPS}", "ro");
-EOF
-      fi
-    fi
-  fi
-
-  # Create events (master only, STATUS for event on slave will be SLAVESIDE_DISABLED)
-  mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
-DROP EVENT IF EXISTS clean_spamalias;
-DELIMITER //
-CREATE EVENT clean_spamalias
-ON SCHEDULE EVERY 1 DAY DO
-BEGIN
-  DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP();
-END;
-//
-DELIMITER ;
-DROP EVENT IF EXISTS clean_oauth2;
-DELIMITER //
-CREATE EVENT clean_oauth2
-ON SCHEDULE EVERY 1 DAY DO
-BEGIN
-  DELETE FROM oauth_refresh_tokens WHERE expires < NOW();
-  DELETE FROM oauth_access_tokens WHERE expires < NOW();
-  DELETE FROM oauth_authorization_codes WHERE expires < NOW();
-END;
-//
-DELIMITER ;
-DROP EVENT IF EXISTS clean_sasl_log;
-DELIMITER //
-CREATE EVENT clean_sasl_log
-ON SCHEDULE EVERY 1 DAY DO
-BEGIN
-  DELETE sasl_log.* FROM sasl_log
-    LEFT JOIN (
-      SELECT username, service, MAX(datetime) AS lastdate
-      FROM sasl_log
-      GROUP BY username, service
-    ) AS last ON sasl_log.username = last.username AND sasl_log.service = last.service
-    WHERE datetime < DATE_SUB(NOW(), INTERVAL 31 DAY) AND datetime < lastdate;
-  DELETE FROM sasl_log
-    WHERE username NOT IN (SELECT username FROM mailbox) AND
-    datetime < DATE_SUB(NOW(), INTERVAL 31 DAY);
-END;
-//
-DELIMITER ;
-EOF
-fi
-
-# Create dummy for custom overrides of mailcow style
-[[ ! -f /web/css/build/0081-custom-mailcow.css ]] && echo '/* Autogenerated by mailcow */' > /web/css/build/0081-custom-mailcow.css
-
-# Fix permissions for global filters
-chown -R 82:82 /global_sieve/*
-
-# Fix permissions on twig cache folder
-chown -R 82:82 /web/templates/cache
-# Clear cache
-find /web/templates/cache/* -not -name '.gitkeep' -delete
-
 # Run hooks
 # Run hooks
 for file in /hooks/*; do
 for file in /hooks/*; do
   if [ -x "${file}" ]; then
   if [ -x "${file}" ]; then
@@ -222,4 +8,13 @@ for file in /hooks/*; do
   fi
   fi
 done
 done
 
 
+python3 -u /bootstrap/main.py
+BOOTSTRAP_EXIT_CODE=$?
+
+if [ $BOOTSTRAP_EXIT_CODE -ne 0 ]; then
+  echo "Bootstrap failed with exit code $BOOTSTRAP_EXIT_CODE. Not starting PHP-FPM."
+  exit $BOOTSTRAP_EXIT_CODE
+fi
+
+echo "Bootstrap succeeded. Starting PHP-FPM..."
 exec "$@"
 exec "$@"

+ 3 - 0
data/conf/phpfpm/config_templates/0081-custom-mailcow.css.j2

@@ -0,0 +1,3 @@
+/*
+  Custom styling for mailcow UI
+*/

+ 0 - 0
data/conf/phpfpm/php-conf.d/opcache-recommended.ini → data/conf/phpfpm/config_templates/opcache-recommended.ini.j2


+ 0 - 0
data/conf/phpfpm/php-conf.d/other.ini → data/conf/phpfpm/config_templates/other.ini.j2


+ 0 - 0
data/conf/phpfpm/php-fpm.d/pools.conf → data/conf/phpfpm/config_templates/pools.conf.j2


+ 2 - 0
data/conf/phpfpm/config_templates/session_store.ini.j2

@@ -0,0 +1,2 @@
+session.save_handler = redis
+session.save_path = "tcp://{{ REDIS_SLAVEOF_IP or 'redis-mailcow' }}:{{ REDIS_SLAVEOF_PORT or '6379' }}?auth={{ REDISPASS }}"

+ 0 - 0
data/conf/phpfpm/php-conf.d/upload.ini → data/conf/phpfpm/config_templates/upload.ini.j2


+ 5 - 8
docker-compose.yml

@@ -131,7 +131,7 @@ services:
             - rspamd
             - rspamd
 
 
     php-fpm-mailcow:
     php-fpm-mailcow:
-      image: ghcr.io/mailcow/phpfpm:1.93
+      image: ghcr.io/mailcow/phpfpm:nightly-19052025
       command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
       command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
       depends_on:
       depends_on:
         - redis-mailcow
         - redis-mailcow
@@ -151,12 +151,8 @@ services:
         - mysql-socket-vol-1:/var/run/mysqld/
         - mysql-socket-vol-1:/var/run/mysqld/
         - ./data/conf/sogo/:/etc/sogo/:z
         - ./data/conf/sogo/:/etc/sogo/:z
         - ./data/conf/rspamd/meta_exporter:/meta_exporter:ro,z
         - ./data/conf/rspamd/meta_exporter:/meta_exporter:ro,z
-        - ./data/conf/phpfpm/crons:/crons:z
+        - ./data/conf/phpfpm:/php-conf:z
         - ./data/conf/phpfpm/sogo-sso/:/etc/sogo-sso/:z
         - ./data/conf/phpfpm/sogo-sso/:/etc/sogo-sso/:z
-        - ./data/conf/phpfpm/php-fpm.d/pools.conf:/usr/local/etc/php-fpm.d/z-pools.conf:Z
-        - ./data/conf/phpfpm/php-conf.d/opcache-recommended.ini:/usr/local/etc/php/conf.d/opcache-recommended.ini:Z
-        - ./data/conf/phpfpm/php-conf.d/upload.ini:/usr/local/etc/php/conf.d/upload.ini:Z
-        - ./data/conf/phpfpm/php-conf.d/other.ini:/usr/local/etc/php/conf.d/zzz-other.ini:Z
         - ./data/conf/dovecot/global_sieve_before:/global_sieve/before:z
         - ./data/conf/dovecot/global_sieve_before:/global_sieve/before:z
         - ./data/conf/dovecot/global_sieve_after:/global_sieve/after:z
         - ./data/conf/dovecot/global_sieve_after:/global_sieve/after:z
         - ./data/assets/templates:/tpls:z
         - ./data/assets/templates:/tpls:z
@@ -164,6 +160,7 @@ services:
       dns:
       dns:
         - ${IPV4_NETWORK:-172.22.1}.254
         - ${IPV4_NETWORK:-172.22.1}.254
       environment:
       environment:
+        - CONTAINER_NAME=php-fpm-mailcow
         - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-}
         - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-}
         - REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-}
         - REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-}
         - REDISPASS=${REDISPASS}
         - REDISPASS=${REDISPASS}
@@ -204,10 +201,10 @@ services:
         ofelia.enabled: "true"
         ofelia.enabled: "true"
         ofelia.job-exec.phpfpm_keycloak_sync.schedule: "@every 1m"
         ofelia.job-exec.phpfpm_keycloak_sync.schedule: "@every 1m"
         ofelia.job-exec.phpfpm_keycloak_sync.no-overlap: "true"
         ofelia.job-exec.phpfpm_keycloak_sync.no-overlap: "true"
-        ofelia.job-exec.phpfpm_keycloak_sync.command: "/bin/bash -c \"php /crons/keycloak-sync.php || exit 0\""
+        ofelia.job-exec.phpfpm_keycloak_sync.command: "/bin/bash -c \"php /php-conf/crons/keycloak-sync.php || exit 0\""
         ofelia.job-exec.phpfpm_ldap_sync.schedule: "@every 1m"
         ofelia.job-exec.phpfpm_ldap_sync.schedule: "@every 1m"
         ofelia.job-exec.phpfpm_ldap_sync.no-overlap: "true"
         ofelia.job-exec.phpfpm_ldap_sync.no-overlap: "true"
-        ofelia.job-exec.phpfpm_ldap_sync.command: "/bin/bash -c \"php /crons/ldap-sync.php || exit 0\""
+        ofelia.job-exec.phpfpm_ldap_sync.command: "/bin/bash -c \"php /php-conf/crons/ldap-sync.php || exit 0\""
       networks:
       networks:
         mailcow-network:
         mailcow-network:
           aliases:
           aliases: