mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2025-12-20 05:11:30 +00:00
[PHP-FPM] use python bootstrapper to start PHP-FPM container
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
@@ -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
data/Dockerfiles/bootstrap/modules/BootstrapPhpfpm.py
Normal file
207
data/Dockerfiles/bootstrap/modules/BootstrapPhpfpm.py
Normal file
@@ -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()
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
COPY data/Dockerfiles/bootstrap /bootstrap
|
||||||
|
COPY data/Dockerfiles/phpfpm/docker-entrypoint.sh /
|
||||||
|
|
||||||
|
RUN chmod +x /docker-entrypoint.sh
|
||||||
|
|
||||||
|
|
||||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||||
|
|
||||||
CMD ["php-fpm"]
|
CMD ["php-fpm"]
|
||||||
|
|||||||
@@ -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 "$@"
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
/*
|
||||||
|
Custom styling for mailcow UI
|
||||||
|
*/
|
||||||
2
data/conf/phpfpm/config_templates/session_store.ini.j2
Normal file
2
data/conf/phpfpm/config_templates/session_store.ini.j2
Normal file
@@ -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 }}"
|
||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user